diff --git a/Gemfile b/Gemfile index cc7d59ef732..724c7143119 100644 --- a/Gemfile +++ b/Gemfile @@ -341,7 +341,6 @@ group :metrics do # Prometheus gem 'prometheus-client-mmap', '~> 0.12.0' - gem 'raindrops', '~> 0.18' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index fcf5acf6c29..6b0c9222858 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1592,7 +1592,6 @@ DEPENDENCIES rails-controller-testing rails-i18n (~> 6.0) rainbow (~> 3.0) - raindrops (~> 0.18) rblineprof (~> 0.3.6) rbtrace (~> 0.4) rdoc (~> 6.1.2) diff --git a/README.md b/README.md index 103c23f152c..bc173b5403c 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ GitLab is a Ruby on Rails application that runs on the following software: - Ubuntu/Debian/CentOS/RHEL/OpenSUSE - Ruby (MRI) 2.7.2 - Git 2.31+ -- Redis 4.0+ +- Redis 5.0+ - PostgreSQL 11+ For more information please see the [architecture](https://docs.gitlab.com/ee/development/architecture.html) and [requirements](https://docs.gitlab.com/ee/install/requirements.html) documentation. diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js index 2cb1b6a195f..9775a9119c6 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js @@ -21,7 +21,7 @@ export const DEFAULT_ITERATIONS = DEFAULT_NONE_ANY.concat([ { value: FILTER_CURRENT, text: __(FILTER_CURRENT) }, ]); -export const DEFAULT_LABELS = [{ value: 'No label', text: __('No label') }]; // eslint-disable-line @gitlab/require-i18n-strings +export const DEFAULT_LABELS = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY]; export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([ { value: 'Upcoming', text: __('Upcoming') }, // eslint-disable-line @gitlab/require-i18n-strings diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue index 6ebc5431012..b325f846934 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue @@ -120,7 +120,7 @@ export default { }, DEBOUNCE_DELAY); }, handleTokenValueSelected(activeTokenValue) { - if (this.isRecentTokenValuesEnabled) { + if (this.isRecentTokenValuesEnabled && activeTokenValue) { setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue); } }, diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue index 76b005772ec..a8868da2dcc 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue @@ -1,27 +1,20 @@ - - - ~{{ activeLabel ? getLabelName(activeLabel) : inputValue }} + ~{{ activeTokenValue ? getLabelName(activeTokenValue) : inputValue }} - + - {{ label.text }} + + + {{ getLabelName(label) }} + - - - - - - - {{ getLabelName(label) }} - - - - + diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index dbe628cb43a..17af44672d9 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -92,7 +92,7 @@ class Projects::BlobController < Projects::ApplicationController @blob.load_all_data! diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true) diff_lines = diffy.diff.scan(/.*\n/)[2..-1] - diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines) + diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines).to_a @diff_lines = Gitlab::Diff::Highlight.new(diff_lines, repository: @repository).highlight render layout: false diff --git a/app/helpers/keyset_helper.rb b/app/helpers/keyset_helper.rb new file mode 100644 index 00000000000..e7f6f884091 --- /dev/null +++ b/app/helpers/keyset_helper.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module KeysetHelper + def keyset_paginate(paginator, without_first_and_last_pages: false) + page_params = params.to_unsafe_h + + render('kaminari/gitlab/keyset_paginator', { + paginator: paginator, + without_first_and_last_pages: without_first_and_last_pages, + page_params: page_params + }) + end +end diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index a28b97e63e5..d1584a62bfb 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -39,6 +39,11 @@ class ProjectHook < WebHook def rate_limit project.actual_limits.limit_for(:web_hook_calls) end + + override :application_context + def application_context + super.merge(project: project) + end end ProjectHook.prepend_mod_with('ProjectHook') diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 02b4feb4ccc..f0c3791079a 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -80,6 +80,11 @@ class WebHook < ApplicationRecord nil end + # Custom attributes to be included in the worker context. + def application_context + { related_class: type } + end + private def web_hooks_disable_failed? diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index c24ab10ad86..82a7e8501cd 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -93,7 +93,9 @@ class WebHookService if rate_limited?(hook) log_rate_limit(hook) else - WebHookWorker.perform_async(hook.id, data, hook_name) + Gitlab::ApplicationContext.with_context(hook.application_context) do + WebHookWorker.perform_async(hook.id, data, hook_name) + end end end diff --git a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json index 7c3720dd2e6..dc08ce9dfad 100644 --- a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json +++ b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json @@ -108,20 +108,6 @@ } ] }, - { - "name": "kubesec", - "label": "Kubesec", - "enabled" : true, - "description": "Kubernetes manifests, Helm Charts", - "variables": [] - }, - { - "name": "nodejs-scan", - "label": "Node.js Scan", - "enabled" : true, - "description": "Node.js", - "variables": [] - }, { "name": "gosec", "label": "Gosec", @@ -139,6 +125,20 @@ } ] }, + { + "name": "kubesec", + "label": "Kubesec", + "enabled" : true, + "description": "Kubernetes manifests, Helm Charts", + "variables": [] + }, + { + "name": "nodejs-scan", + "label": "Node.js Scan", + "enabled" : true, + "description": "Node.js", + "variables": [] + }, { "name": "phpcs-security-audit", "label": "PHP Security Audit", diff --git a/app/views/kaminari/gitlab/_keyset_paginator.html.haml b/app/views/kaminari/gitlab/_keyset_paginator.html.haml new file mode 100644 index 00000000000..f64c70dadfc --- /dev/null +++ b/app/views/kaminari/gitlab/_keyset_paginator.html.haml @@ -0,0 +1,30 @@ +- previous_path = url_for(page_params.merge(cursor: paginator.cursor_for_previous_page)) +- next_path = url_for(page_params.merge(cursor: paginator.cursor_for_next_page)) + +.gl-pagination.gl-mt-3 + %ul.pagination.justify-content-center + + - if paginator.has_previous_page? + - unless without_first_and_last_pages + %li.page-item + - first_page_path = url_for(page_params.merge(cursor: paginator.cursor_for_first_page)) + = link_to first_page_path, rel: 'first', class: 'page-link' do + = sprite_icon('angle-double-left', size: 8) + = s_('Pagination|First') + + %li.page-item.prev + = link_to previous_path, rel: 'prev', class: 'page-link' do + = sprite_icon('angle-left', size: 8) + = s_('Pagination|Prev') + + - if paginator.has_next_page? + %li.page-item.next + = link_to next_path, rel: 'next', class: 'page-link' do + = s_('Pagination|Next') + = sprite_icon('angle-right', size: 8) + - unless without_first_and_last_pages + %li.page-item + - last_page_path = url_for(page_params.merge(cursor: paginator.cursor_for_last_page)) + = link_to last_page_path, rel: 'last', class: 'page-link' do + = s_('Pagination|Last') + = sprite_icon('angle-double-right', size: 8) diff --git a/changelogs/unreleased/330402-remove-unicorn-gitlab-unicorn-sampler.yml b/changelogs/unreleased/330402-remove-unicorn-gitlab-unicorn-sampler.yml new file mode 100644 index 00000000000..1773ea98cab --- /dev/null +++ b/changelogs/unreleased/330402-remove-unicorn-gitlab-unicorn-sampler.yml @@ -0,0 +1,5 @@ +--- +title: Remove Unicorn Sampler +merge_request: 62090 +author: +type: removed diff --git a/changelogs/unreleased/330737_fix_blob_preview.yml b/changelogs/unreleased/330737_fix_blob_preview.yml new file mode 100644 index 00000000000..8c322dce5bb --- /dev/null +++ b/changelogs/unreleased/330737_fix_blob_preview.yml @@ -0,0 +1,5 @@ +--- +title: Fix blob preview error +merge_request: 62128 +author: +type: fixed diff --git a/changelogs/unreleased/dz-redirect-old-repo-routes.yml b/changelogs/unreleased/dz-redirect-old-repo-routes.yml new file mode 100644 index 00000000000..a6c72e43433 --- /dev/null +++ b/changelogs/unreleased/dz-redirect-old-repo-routes.yml @@ -0,0 +1,5 @@ +--- +title: Redirect some of deprecated repository routes +merge_request: 34867 +author: +type: removed diff --git a/changelogs/unreleased/handle-missing-branch-error.yml b/changelogs/unreleased/handle-missing-branch-error.yml new file mode 100644 index 00000000000..01c6362bbe8 --- /dev/null +++ b/changelogs/unreleased/handle-missing-branch-error.yml @@ -0,0 +1,5 @@ +--- +title: Return 404 from branches API when repository does not exist +merge_request: +author: +type: fixed diff --git a/config/feature_flags/development/ci_drop_cyclical_triggered_pipelines.yml b/config/feature_flags/development/ci_drop_cyclical_triggered_pipelines.yml index 6a08d4aa72c..6d411dd377f 100644 --- a/config/feature_flags/development/ci_drop_cyclical_triggered_pipelines.yml +++ b/config/feature_flags/development/ci_drop_cyclical_triggered_pipelines.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329390 milestone: '13.12' type: development group: group::continuous integration -default_enabled: true +default_enabled: false diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index aee9dd455af..81eb9878376 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -33,7 +33,7 @@ production: &base host: localhost port: 80 # Set to 443 if using HTTPS, see installation.md#using-https for additional HTTPS configuration details https: false # Set to true if using HTTPS, see installation.md#using-https for additional HTTPS configuration details - # The maximum time unicorn/puma can spend on the request. This needs to be smaller than the worker timeout. + # The maximum time Puma can spend on the request. This needs to be smaller than the worker timeout. # Default is 95% of the worker timeout max_request_duration_seconds: 57 @@ -153,7 +153,7 @@ production: &base ### GraphQL Settings # Tells the rails application how long it has to complete a GraphQL request. # We suggest this value to be higher than the database timeout value - # and lower than the worker timeout set in unicorn/puma. (default: 30) + # and lower than the worker timeout set in Puma. (default: 30) # graphql_timeout: 30 ## Repository downloads directory @@ -1212,8 +1212,6 @@ production: &base ## Monitoring # Built in monitoring settings monitoring: - # Time between sampling of unicorn socket metrics, in seconds - # unicorn_sampler_interval: 10 # IP whitelist to access monitoring endpoints ip_whitelist: - 127.0.0.0/8 @@ -1225,7 +1223,7 @@ production: &base # address: localhost # port: 8082 - # Web exporter is webserver built in to Unicorn/Puma to expose Prometheus metrics + # Web exporter is a dedicated Rack server running alongside Puma to expose Prometheus metrics # It runs alongside the `/metrics` endpoints to ease the publish of metrics web_exporter: # enabled: true diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 8d5e19afc20..c00d0c04e5f 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -902,7 +902,6 @@ Settings.webpack.dev_server['https'] ||= false # Settings['monitoring'] ||= Settingslogic.new({}) Settings.monitoring['ip_whitelist'] ||= ['127.0.0.1/8'] -Settings.monitoring['unicorn_sampler_interval'] ||= 10 Settings.monitoring['sidekiq_exporter'] ||= Settingslogic.new({}) Settings.monitoring.sidekiq_exporter['enabled'] ||= false Settings.monitoring.sidekiq_exporter['log_enabled'] ||= false diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb index a304f861db8..35acf26c796 100644 --- a/config/initializers/7_prometheus_metrics.rb +++ b/config/initializers/7_prometheus_metrics.rb @@ -8,8 +8,6 @@ def prometheus_default_multiproc_dir if Gitlab::Runtime.sidekiq? Rails.root.join('tmp/prometheus_multiproc_dir/sidekiq') - elsif Gitlab::Runtime.unicorn? - Rails.root.join('tmp/prometheus_multiproc_dir/unicorn') elsif Gitlab::Runtime.puma? Rails.root.join('tmp/prometheus_multiproc_dir/puma') else @@ -49,9 +47,7 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled? ::Prometheus::Client.reinitialize_on_pid_change(force: true) - if Gitlab::Runtime.unicorn? - Gitlab::Metrics::Samplers::UnicornSampler.instance(Settings.monitoring.unicorn_sampler_interval).start - elsif Gitlab::Runtime.puma? + if Gitlab::Runtime.puma? Gitlab::Metrics::Samplers::PumaSampler.instance.start end diff --git a/config/initializers/active_record_keyset_pagination.rb b/config/initializers/active_record_keyset_pagination.rb new file mode 100644 index 00000000000..f8c2ada5255 --- /dev/null +++ b/config/initializers/active_record_keyset_pagination.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module PaginatorExtension + # This method loads the records for the requested page and returns a keyset paginator object. + def keyset_paginate(cursor: nil, per_page: 20) + Gitlab::Pagination::Keyset::Paginator.new(scope: self.dup, cursor: cursor, per_page: per_page) + end +end + +ActiveSupport.on_load(:active_record) do + ActiveRecord::Relation.include(PaginatorExtension) +end diff --git a/config/metrics/counts_all/20210216174832_cycle_analytics_views.yml b/config/metrics/counts_all/20210216174832_cycle_analytics_views.yml index 77baa673bb2..ce8674574f2 100644 --- a/config/metrics/counts_all/20210216174832_cycle_analytics_views.yml +++ b/config/metrics/counts_all/20210216174832_cycle_analytics_views.yml @@ -1,6 +1,6 @@ --- key_path: counts.cycle_analytics_views -description: +description: Total visits to VSA (both group- and project-level) all time product_section: dev product_stage: manage product_group: group::optimize @@ -8,9 +8,11 @@ product_category: value_type: number status: data_available time_frame: all -data_source: database +data_source: redis distribution: - ce +- ee tier: - free -skip_validation: true +- premium +- ultimate diff --git a/config/routes/project.rb b/config/routes/project.rb index d62e2f1b2f2..935e816f73c 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -386,7 +386,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do # The wiki and repository routing contains wildcard characters so # its preferable to keep it below all other project routes draw :repository_scoped - draw :repository draw :wiki namespace :import do @@ -584,6 +583,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do # Introduced in 12.0. # Should be removed with https://gitlab.com/gitlab-org/gitlab/issues/28848. Gitlab::Routing.redirect_legacy_paths(self, :mirror, :tags, :hooks, + :commits, :commit, :find_file, :files, :compare, :cycle_analytics, :mattermost, :variables, :triggers, :environments, :protected_environments, :error_tracking, :alert_management, :tracing, diff --git a/config/routes/repository.rb b/config/routes/repository.rb index 58de3d29bb0..854c6517cd5 100644 --- a/config/routes/repository.rb +++ b/config/routes/repository.rb @@ -1,84 +1,36 @@ # frozen_string_literal: true # All routing related to repository browsing +# +# NOTE: Add new routes to repository_scoped.rb instead (see +# https://docs.gitlab.com/ee/development/routing.html#project-routes). resource :repository, only: [:create] -resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do - member do - get :branches - get :pipelines - post :revert - post :cherry_pick - get :diff_for_path - get :diff_files - get :merge_requests - end -end - -# NOTE: Add new routes to repository_scoped.rb instead (see -# https://docs.gitlab.com/ee/development/routing.html#project-routes). -# # Don't use format parameter as file extension (old 3.0.x behavior) # See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments scope format: false do - get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ } + get '/refs/switch', + to: redirect('%{namespace_id}/%{project_id}/-/refs/switch') - resources :compare, only: [:index, :create] do - collection do - get :diff_for_path - get :signatures - end - end + get '/refs/:id/logs_tree', + to: redirect('%{namespace_id}/%{project_id}/-/refs/%{id}/logs_tree'), + constraints: { id: Gitlab::PathRegex.git_reference_regex } - resources :refs, only: [] do - collection do - get 'switch' - end - - member do - # tree viewer logs - get 'logs_tree', constraints: { id: Gitlab::PathRegex.git_reference_regex } - # Directories with leading dots erroneously get rejected if git - # ref regex used in constraints. Regex verification now done in controller. - get 'logs_tree/*path', action: :logs_tree, as: :logs_file, format: false, constraints: { - id: /.*/, - path: /[^\0]*/ - } - end - end + get '/refs/:id/logs_tree/*path', + to: redirect('%{namespace_id}/%{project_id}/-/refs/%{id}/logs_tree/%{path}'), + constraints: { id: /.*/, path: /[^\0]*/ } scope constraints: { id: /[^\0]+/ } do - scope controller: :blob do - get '/new/*id', action: :new, as: :new_blob - post '/create/*id', action: :create, as: :create_blob - get '/edit/*id', action: :edit, as: :edit_blob - put '/update/*id', action: :update, as: :update_blob - post '/preview/*id', action: :preview, as: :preview_blob + # Deprecated. Keep for compatibility. + # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/118849 + get '/tree/*id', to: 'tree#show', as: :deprecated_tree + get '/blob/*id', to: 'blob#show', as: :deprecated_blob + get '/raw/*id', to: 'raw#show', as: :deprecated_raw + get '/blame/*id', to: 'blame#show', as: :deprecated_blame - scope path: '/blob/*id', as: :blob do - get :diff - get '/', action: :show - delete '/', action: :destroy - post '/', action: :create - put '/', action: :update - end - end - - get '/tree/*id', to: 'tree#show', as: :tree - get '/raw/*id', to: 'raw#show', as: :raw - get '/blame/*id', to: 'blame#show', as: :blame - - get '/commits', to: 'commits#commits_root', as: :commits_root - get '/commits/*id/signatures', to: 'commits#signatures', as: :signatures - get '/commits/*id', to: 'commits#show', as: :commits - - post '/create_dir/*id', to: 'tree#create_dir', as: :create_dir - - scope controller: :find_file do - get '/find_file/*id', action: :show, as: :find_file - - get '/files/*id', action: :list, as: :files - end + # Redirect those explicitly since `redirect_legacy_paths` conflicts with project new/edit actions + get '/new/*id', to: redirect('%{namespace_id}/%{project_id}/-/new/%{id}') + get '/edit/*id', to: redirect('%{namespace_id}/%{project_id}/-/edit/%{id}') end end diff --git a/config/routes/repository_scoped.rb b/config/routes/repository_scoped.rb index 7fabf3ff895..d2be18c62f9 100644 --- a/config/routes/repository_scoped.rb +++ b/config/routes/repository_scoped.rb @@ -6,6 +6,33 @@ # Don't use format parameter as file extension (old 3.0.x behavior) # See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments scope format: false do + get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ } + + resources :compare, only: [:index, :create] do + collection do + get :diff_for_path + get :signatures + end + end + + resources :refs, only: [] do + collection do + get 'switch' + end + + member do + # tree viewer logs + get 'logs_tree', constraints: { id: Gitlab::PathRegex.git_reference_regex } + + # Directories with leading dots erroneously get rejected if git + # ref regex used in constraints. Regex verification now done in controller. + get 'logs_tree/*path', action: :logs_tree, as: :logs_file, format: false, constraints: { + id: /.*/, + path: /[^\0]*/ + } + end + end + scope constraints: { id: Gitlab::PathRegex.git_reference_regex } do resources :network, only: [:show] @@ -38,4 +65,51 @@ scope format: false do end end end + + scope constraints: { id: /[^\0]+/ } do + scope controller: :blob do + get '/new/*id', action: :new, as: :new_blob + post '/create/*id', action: :create, as: :create_blob + get '/edit/*id', action: :edit, as: :edit_blob + put '/update/*id', action: :update, as: :update_blob + post '/preview/*id', action: :preview, as: :preview_blob + + scope path: '/blob/*id', as: :blob do + get :diff + get '/', action: :show + delete '/', action: :destroy + post '/', action: :create + put '/', action: :update + end + end + + get '/tree/*id', to: 'tree#show', as: :tree + get '/raw/*id', to: 'raw#show', as: :raw + get '/blame/*id', to: 'blame#show', as: :blame + + get '/commits', to: 'commits#commits_root', as: :commits_root + get '/commits/*id/signatures', to: 'commits#signatures', as: :signatures + get '/commits/*id', to: 'commits#show', as: :commits + + post '/create_dir/*id', to: 'tree#create_dir', as: :create_dir + + scope controller: :find_file do + get '/find_file/*id', action: :show, as: :find_file + get '/files/*id', action: :list, as: :files + end + end end + +resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do + member do + get :branches + get :pipelines + post :revert + post :cherry_pick + get :diff_for_path + get :diff_files + get :merge_requests + end +end + +resource :repository, only: [:create] diff --git a/doc/administration/monitoring/performance/index.md b/doc/administration/monitoring/performance/index.md index 15c54a36f6c..f3db6ac9f03 100644 --- a/doc/administration/monitoring/performance/index.md +++ b/doc/administration/monitoring/performance/index.md @@ -69,6 +69,5 @@ The following environment variables are recognized: - `DATABASE_SAMPLER_INTERVAL_SECONDS` - `ACTION_CABLE_SAMPLER_INTERVAL_SECONDS` - `PUMA_SAMPLER_INTERVAL_SECONDS` -- `UNICORN_SAMPLER_INTERVAL_SECONDS` - `THREADS_SAMPLER_INTERVAL_SECONDS` - `GLOBAL_SEARCH_SAMPLER_INTERVAL_SECONDS` diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index f29db9ead38..faa24e76a0e 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -308,20 +308,8 @@ Some basic Ruby runtime metrics are available: | `ruby_process_proportional_memory_bytes` | Gauge | 13.0 | Memory usage by process (PSS/Proportional Set Size) | | `ruby_process_start_time_seconds` | Gauge | 12.0 | UNIX timestamp of process start time | -## Unicorn Metrics - -Unicorn specific metrics, when Unicorn is used. - -| Metric | Type | Since | Description | -|:-----------------------------|:------|:------|:---------------------------------------------------| -| `unicorn_active_connections` | Gauge | 11.0 | The number of active Unicorn connections (workers) | -| `unicorn_queued_connections` | Gauge | 11.0 | The number of queued Unicorn connections | -| `unicorn_workers` | Gauge | 12.0 | The number of Unicorn workers | - ## Puma Metrics -When Puma is used instead of Unicorn, the following metrics are available: - | Metric | Type | Since | Description | |:--------------------------------- |:------- |:----- |:----------- | | `puma_workers` | Gauge | 12.0 | Total number of workers | @@ -352,8 +340,8 @@ instance (`cache`, `shared_state` etc.). ## Metrics shared directory The GitLab Prometheus client requires a directory to store metrics data shared between multi-process services. -Those files are shared among all instances running under Unicorn server. -The directory must be accessible to all running Unicorn's processes, or +Those files are shared among all instances running under Puma server. +The directory must be accessible to all running Puma's processes, or metrics can't function correctly. This directory's location is configured using environment variable `prometheus_multiproc_dir`. diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md index 588be73e786..dfa17b4e85d 100644 --- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md +++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md @@ -863,55 +863,6 @@ license.save License.current # check to make sure it applied ``` -## Unicorn - -From [Zendesk ticket #91083](https://gitlab.zendesk.com/agent/tickets/91083) (internal) - -### Poll Unicorn requests by seconds - -```ruby -require 'rubygems' -require 'unicorn' - -# Usage for this program -def usage - puts "ruby unicorn_status.rb " - puts "Polls the given Unix socket every interval in seconds. Will not allow you to drop below 3 second poll intervals." - puts "Example: /opt/gitlab/embedded/bin/ruby poll_unicorn.rb /var/opt/gitlab/gitlab-rails/sockets/gitlab.socket 10" -end - -# Look for required args. Throw usage and exit if they don't exist. -if ARGV.count < 2 - usage - exit 1 -end - -# Get the socket and threshold values. -socket = ARGV[0] -threshold = (ARGV[1]).to_i - -# Check threshold - is it less than 3? If so, set to 3 seconds. Safety first! -if threshold.to_i < 3 - threshold = 3 -end - -# Check - does that socket exist? -unless File.exist?(socket) - puts "Socket file not found: #{socket}" - exit 1 -end - -# Poll the given socket every THRESHOLD seconds as specified above. -puts "Running infinite loop. Use CTRL+C to exit." -puts "------------------------------------------" -loop do - Raindrops::Linux.unix_listener_stats([socket]).each do |addr, stats| - puts DateTime.now.to_s + " Active: " + stats.active.to_s + " Queued: " + stats.queued.to_s - end - sleep threshold -end -``` - ## Registry ### Registry Disk Space Usage by Project diff --git a/doc/development/snowplow/index.md b/doc/development/snowplow/index.md index ece72dbbf03..6e2bf00f191 100644 --- a/doc/development/snowplow/index.md +++ b/doc/development/snowplow/index.md @@ -444,7 +444,15 @@ There are several tools for developing and testing Snowplow Event **{check-circle}** Available, **{status_preparing}** In progress, **{dotted-circle}** Not Planned -### Snowplow Analytics Debugger Chrome Extension +### Test frontend events + +To test frontend events in development: + +- [Enable Snowplow in the admin area](#enabling-snowplow). +- Turn off any ad blockers that would prevent Snowplow JS from loading in your environment. +- Turn off "Do Not Track" (DNT) in your browser. + +#### Snowplow Analytics Debugger Chrome Extension Snowplow Analytics Debugger is a browser extension for testing frontend events. This works on production, staging and local development environments. @@ -452,7 +460,7 @@ Snowplow Analytics Debugger is a browser extension for testing frontend events. 1. Open Chrome DevTools to the Snowplow Analytics Debugger tab. 1. Learn more at [Igloo Analytics](https://www.iglooanalytics.com/blog/snowplow-analytics-debugger-chrome-extension.html). -### Snowplow Inspector Chrome Extension +#### Snowplow Inspector Chrome Extension Snowplow Inspector Chrome Extension is a browser extension for testing frontend events. This works on production, staging and local development environments. diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index 250945ac47f..c69b9e76103 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -936,7 +936,7 @@ Tiers: `free` ### `counts.cycle_analytics_views` -Missing description +Total visits to VSA (both group- and project-level) all time [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216174832_cycle_analytics_views.yml) @@ -944,7 +944,7 @@ Group: `group::optimize` Status: `data_available` -Tiers: `free` +Tiers: `free`, `premium`, `ultimate` ### `counts.dast_jobs` @@ -4128,7 +4128,7 @@ Tiers: `free`, `premium`, `ultimate` ### `counts.productivity_analytics_views` -Missing description +Total visits to /groups/:group/-/analytics/productivity_analytics all time [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_all/20210216174834_productivity_analytics_views.yml) @@ -4136,7 +4136,7 @@ Group: `group::optimize` Status: `data_available` -Tiers: `free` +Tiers: `free`, `premium`, `ultimate` ### `counts.project_clusters_disabled` diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 1ee120f982a..0db5bb82296 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -17,6 +17,10 @@ module API authorize! :download_code, user_project end + rescue_from Gitlab::Git::Repository::NoRepository do + not_found! + end + helpers do params :filter_params do optional :search, type: String, desc: 'Return list of branches matching the search criteria' diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml index c628e30b2c7..92e74f9eb00 100644 --- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml @@ -1,10 +1,21 @@ -# Read more about this feature here: https://docs.gitlab.com/ee/user/application_security/container_scanning/ +# Use this template to enable container scanning in your project. +# You should add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword. +# The template should work without modifications but you can customize the template settings if +# needed: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings +# +# Requirements: +# - You must define the image to be scanned in the DOCKER_IMAGE variable. If DOCKER_IMAGE is the +# same as $CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG, you can skip this. +# - Container registry credentials defined by `DOCKER_USER` and `DOCKER_PASSWORD` variables if the image to be scanned is in a private registry. +# - For auto-remediation, a readable Dockerfile in the root of the project or as defined by the +# DOCKERFILE_PATH variable. +# +# For more information, see https://docs.gitlab.com/ee/user/application_security/container_scanning/#available-variables variables: - # Setting this variable will affect all Security templates - # (SAST, Dependency Scanning, ...) + # Setting this variable will affect all Security templates (e.g.: SAST, Dependency Scanning) SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers" - CS_MAJOR_VERSION: 3 + CS_MAJOR_VERSION: 3 # The major version of the analyzer image to be used for scanning .cs_common: stage: test diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb deleted file mode 100644 index 2fa324f3fea..00000000000 --- a/lib/gitlab/metrics/samplers/unicorn_sampler.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Metrics - module Samplers - class UnicornSampler < BaseSampler - DEFAULT_SAMPLING_INTERVAL_SECONDS = 5 - - def metrics - @metrics ||= init_metrics - end - - def init_metrics - { - unicorn_active_connections: ::Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max), - unicorn_queued_connections: ::Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max), - unicorn_workers: ::Gitlab::Metrics.gauge(:unicorn_workers, 'Unicorn workers') - } - end - - def enabled? - # Raindrops::Linux.tcp_listener_stats is only present on Linux - unicorn_with_listeners? && Raindrops::Linux.respond_to?(:tcp_listener_stats) - end - - def sample - Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats| - set_unicorn_connection_metrics('tcp', addr, stats) - end - Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats| - set_unicorn_connection_metrics('unix', addr, stats) - end - - metrics[:unicorn_workers].set({}, unicorn_workers_count) - end - - private - - def tcp_listeners - @tcp_listeners ||= Unicorn.listener_names.grep(%r{\A[^/]+:\d+\z}) - end - - def set_unicorn_connection_metrics(type, addr, stats) - labels = { socket_type: type, socket_address: addr } - - metrics[:unicorn_active_connections].set(labels, stats.active) - metrics[:unicorn_queued_connections].set(labels, stats.queued) - end - - def unix_listeners - @unix_listeners ||= Unicorn.listener_names - tcp_listeners - end - - def unicorn_with_listeners? - defined?(Unicorn) && Unicorn.listener_names.any? - end - - def unicorn_workers_count - http_servers.sum(&:worker_processes) - end - - # Traversal of ObjectSpace is expensive, on fully loaded application - # it takes around 80ms. The instances of HttpServers are not a subject - # to change so we can cache the list of servers. - def http_servers - return [] unless Gitlab::Runtime.unicorn? - - @http_servers ||= ObjectSpace.each_object(::Unicorn::HttpServer).to_a - end - end - end - end -end diff --git a/lib/gitlab/pagination/keyset/paginator.rb b/lib/gitlab/pagination/keyset/paginator.rb new file mode 100644 index 00000000000..2ec4472fcd6 --- /dev/null +++ b/lib/gitlab/pagination/keyset/paginator.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +module Gitlab + module Pagination + module Keyset + class Paginator + include Enumerable + + module Base64CursorConverter + def self.dump(cursor_attributes) + Base64.urlsafe_encode64(Gitlab::Json.dump(cursor_attributes)) + end + + def self.parse(cursor) + Gitlab::Json.parse(Base64.urlsafe_decode64(cursor)).with_indifferent_access + end + end + + FORWARD_DIRECTION = 'n' + BACKWARD_DIRECTION = 'p' + + UnsupportedScopeOrder = Class.new(StandardError) + + # scope - ActiveRecord::Relation object with order by clause + # cursor - Encoded cursor attributes as String. Empty value will requests the first page. + # per_page - Number of items per page. + # cursor_converter - Object that serializes and de-serializes the cursor attributes. Implements dump and parse methods. + # direction_key - Symbol that will be the hash key of the direction within the cursor. (default: _kd => keyset direction) + def initialize(scope:, cursor: nil, per_page: 20, cursor_converter: Base64CursorConverter, direction_key: :_kd) + @keyset_scope = build_scope(scope) + @order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(@keyset_scope) + @per_page = per_page + @cursor_converter = cursor_converter + @direction_key = direction_key + @has_another_page = false + @at_last_page = false + @at_first_page = false + @cursor_attributes = decode_cursor_attributes(cursor) + + set_pagination_helper_flags! + end + + # rubocop: disable CodeReuse/ActiveRecord + def records + @records ||= begin + items = if paginate_backward? + reversed_order + .apply_cursor_conditions(keyset_scope, cursor_attributes) + .reorder(reversed_order) + .limit(per_page_plus_one) + .to_a + else + order + .apply_cursor_conditions(keyset_scope, cursor_attributes) + .limit(per_page_plus_one) + .to_a + end + + @has_another_page = items.size == per_page_plus_one + items.pop if @has_another_page + items.reverse! if paginate_backward? + items + end + end + # rubocop: enable CodeReuse/ActiveRecord + + # This and has_previous_page? methods are direction aware. In case we paginate backwards, + # has_next_page? will mean that we have a previous page. + def has_next_page? + records + + if at_last_page? + false + elsif paginate_forward? + @has_another_page + elsif paginate_backward? + true + end + end + + def has_previous_page? + records + + if at_first_page? + false + elsif paginate_backward? + @has_another_page + elsif paginate_forward? + true + end + end + + def cursor_for_next_page + if has_next_page? + data = order.cursor_attributes_for_node(records.last) + data[direction_key] = FORWARD_DIRECTION + cursor_converter.dump(data) + else + nil + end + end + + def cursor_for_previous_page + if has_previous_page? + data = order.cursor_attributes_for_node(records.first) + data[direction_key] = BACKWARD_DIRECTION + cursor_converter.dump(data) + end + end + + def cursor_for_first_page + cursor_converter.dump({ direction_key => FORWARD_DIRECTION }) + end + + def cursor_for_last_page + cursor_converter.dump({ direction_key => BACKWARD_DIRECTION }) + end + + delegate :each, :empty?, :any?, to: :records + + private + + attr_reader :keyset_scope, :order, :per_page, :cursor_converter, :direction_key, :cursor_attributes + + delegate :reversed_order, to: :order + + def at_last_page? + @at_last_page + end + + def at_first_page? + @at_first_page + end + + def per_page_plus_one + per_page + 1 + end + + def decode_cursor_attributes(cursor) + cursor.blank? ? {} : cursor_converter.parse(cursor) + end + + def set_pagination_helper_flags! + @direction = cursor_attributes.delete(direction_key.to_s) + + if cursor_attributes.blank? && @direction.blank? + @at_first_page = true + @direction = FORWARD_DIRECTION + elsif cursor_attributes.blank? + if paginate_forward? + @at_first_page = true + else + @at_last_page = true + end + end + end + + def paginate_backward? + @direction == BACKWARD_DIRECTION + end + + def paginate_forward? + @direction == FORWARD_DIRECTION + end + + def build_scope(scope) + keyset_aware_scope, success = Gitlab::Pagination::Keyset::SimpleOrderBuilder.build(scope) + + raise(UnsupportedScopeOrder, 'The order on the scope does not support keyset pagination') unless success + + keyset_aware_scope + end + end + end + end +end diff --git a/lib/gitlab/pagination/keyset/simple_order_builder.rb b/lib/gitlab/pagination/keyset/simple_order_builder.rb index 5ac5737c3be..76d6bbadaa4 100644 --- a/lib/gitlab/pagination/keyset/simple_order_builder.rb +++ b/lib/gitlab/pagination/keyset/simple_order_builder.rb @@ -26,6 +26,8 @@ module Gitlab def build order = if order_values.empty? primary_key_descending_order + elsif Gitlab::Pagination::Keyset::Order.keyset_aware?(scope) + Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(scope) elsif ordered_by_primary_key? primary_key_order elsif ordered_by_other_column? diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 56965ae57c1..5c9754b74ec 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -23703,6 +23703,9 @@ msgstr "" msgid "Pages Domain" msgstr "" +msgid "Pagination|First" +msgstr "" + msgid "Pagination|Go to first page" msgstr "" @@ -23715,6 +23718,9 @@ msgstr "" msgid "Pagination|Go to previous page" msgstr "" +msgid "Pagination|Last" +msgstr "" + msgid "Pagination|Last ยป" msgstr "" diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index b965feee645..9493215247a 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -444,6 +444,40 @@ RSpec.describe Projects::BlobController do end end + describe 'POST preview' do + subject(:request) { post :preview, params: default_params } + + let(:user) { create(:user) } + let(:filename) { 'preview.md' } + let(:default_params) do + { + namespace_id: project.namespace, + project_id: project, + id: "#{project.default_branch}/#{filename}", + content: "Bar\n" + } + end + + before do + project.add_developer(user) + sign_in(user) + + project.repository.create_file( + project.creator, + filename, + "Foo\n", + message: 'Test', + branch_name: project.default_branch + ) + end + + it 'is successful' do + request + + expect(response).to be_successful + end + end + describe 'POST create' do let(:user) { create(:user) } let(:default_params) do diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js index 57514a0c499..b3cb3142911 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js @@ -1,5 +1,4 @@ import { - GlFilteredSearchToken, GlFilteredSearchSuggestion, GlFilteredSearchTokenSegment, GlDropdownDivider, @@ -18,6 +17,7 @@ import { DEFAULT_LABELS, DEFAULT_NONE_ANY, } from '~/vue_shared/components/filtered_search_bar/constants'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import { mockLabelToken } from '../mock_data'; @@ -25,6 +25,7 @@ import { mockLabelToken } from '../mock_data'; jest.mock('~/flash'); const defaultStubs = { Portal: true, + BaseToken, GlFilteredSearchSuggestionList: { template: '', methods: { @@ -68,55 +69,17 @@ describe('LabelToken', () => { wrapper.destroy(); }); - describe('computed', () => { - beforeEach(async () => { - // Label title with spaces is always enclosed in quotations by component. - wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } }); - - wrapper.setData({ - labels: mockLabels, - }); - - await wrapper.vm.$nextTick(); - }); - - describe('currentValue', () => { - it('returns lowercase string for `value.data`', () => { - expect(wrapper.vm.currentValue).toBe('"foo label"'); - }); - }); - - describe('activeLabel', () => { - it('returns object for currently present `value.data`', () => { - expect(wrapper.vm.activeLabel).toEqual(mockRegularLabel); - }); - }); - - describe('containerStyle', () => { - it('returns object containing `backgroundColor` and `color` properties based on `activeLabel` value', () => { - expect(wrapper.vm.containerStyle).toEqual({ - backgroundColor: mockRegularLabel.color, - color: mockRegularLabel.textColor, - }); - }); - - it('returns empty object when `activeLabel` is not set', async () => { - wrapper.setData({ - labels: [], - }); - - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.containerStyle).toEqual({}); - }); - }); - }); - describe('methods', () => { beforeEach(() => { wrapper = createComponent(); }); + describe('getActiveLabel', () => { + it('returns label object from labels array based on provided `currentValue` param', () => { + expect(wrapper.vm.getActiveLabel(mockLabels, 'foo label')).toEqual(mockRegularLabel); + }); + }); + describe('getLabelName', () => { it('returns value of `name` or `title` property present in provided label param', () => { let mockLabel = { @@ -187,8 +150,14 @@ describe('LabelToken', () => { await wrapper.vm.$nextTick(); }); - it('renders gl-filtered-search-token component', () => { - expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true); + it('renders base-token component', () => { + const baseTokenEl = wrapper.find(BaseToken); + + expect(baseTokenEl.exists()).toBe(true); + expect(baseTokenEl.props()).toMatchObject({ + tokenValues: mockLabels, + fnActiveTokenValue: wrapper.vm.getActiveLabel, + }); }); it('renders token item when value is selected', () => { diff --git a/spec/helpers/keyset_helper_spec.rb b/spec/helpers/keyset_helper_spec.rb new file mode 100644 index 00000000000..2e4bf537e2f --- /dev/null +++ b/spec/helpers/keyset_helper_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe KeysetHelper, type: :controller do + controller(Admin::UsersController) do + def index + @users = User + .where(admin: false) + .order(id: :desc) + .keyset_paginate(cursor: params[:cursor], per_page: 2) + + render inline: "<%= keyset_paginate @users %>", layout: false # rubocop: disable Rails/RenderInline + end + end + + render_views + + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + context 'with admin mode', :enable_admin_mode do + context 'when no users are present' do + it 'does not render pagination links' do + get :index + + expect(response.body).not_to include(s_('Pagination|First')) + expect(response.body).not_to include(s_('Pagination|Prev')) + expect(response.body).not_to include(s_('Pagination|Next')) + expect(response.body).not_to include(s_('Pagination|Last')) + end + end + + context 'when one user is present' do + before do + create(:user) + end + + it 'does not render pagination links' do + get :index + + expect(response.body).not_to include(s_('Pagination|First')) + expect(response.body).not_to include(s_('Pagination|Prev')) + expect(response.body).not_to include(s_('Pagination|Next')) + expect(response.body).not_to include(s_('Pagination|Last')) + end + end + + context 'when more users are present' do + let_it_be(:users) { create_list(:user, 5) } + + let(:paginator) { User.where(admin: false).order(id: :desc).keyset_paginate(per_page: 2) } + + context 'when on the first page' do + it 'renders the next and last links' do + get :index + + expect(response.body).not_to include(s_('Pagination|First')) + expect(response.body).not_to include(s_('Pagination|Prev')) + expect(response.body).to include(s_('Pagination|Next')) + expect(response.body).to include(s_('Pagination|Last')) + end + end + + context 'when at the last page' do + it 'renders the prev and first links' do + cursor = paginator.cursor_for_last_page + + get :index, params: { cursor: cursor } + + expect(response.body).to include(s_('Pagination|First')) + expect(response.body).to include(s_('Pagination|Prev')) + expect(response.body).not_to include(s_('Pagination|Next')) + expect(response.body).not_to include(s_('Pagination|Last')) + end + end + + context 'when at the second page' do + it 'renders all links' do + cursor = paginator.cursor_for_next_page + + get :index, params: { cursor: cursor } + + expect(response.body).to include(s_('Pagination|First')) + expect(response.body).to include(s_('Pagination|Prev')) + expect(response.body).to include(s_('Pagination|Next')) + expect(response.body).to include(s_('Pagination|Last')) + end + end + end + end +end diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb deleted file mode 100644 index 7971a7cabd5..00000000000 --- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb +++ /dev/null @@ -1,141 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::Samplers::UnicornSampler do - subject { described_class.new(1.second) } - - it_behaves_like 'metrics sampler', 'UNICORN_SAMPLER' - - describe '#sample' do - let(:unicorn) { Module.new } - let(:raindrops) { double('raindrops') } - let(:stats) { double('stats') } - - before do - stub_const('Unicorn', unicorn) - stub_const('Raindrops::Linux', raindrops) - allow(raindrops).to receive(:unix_listener_stats).and_return({}) - allow(raindrops).to receive(:tcp_listener_stats).and_return({}) - end - - context 'unicorn listens on unix sockets' do - let(:socket_address) { '/some/sock' } - let(:sockets) { [socket_address] } - - before do - allow(unicorn).to receive(:listener_names).and_return(sockets) - end - - it 'samples socket data' do - expect(raindrops).to receive(:unix_listener_stats).with(sockets) - - subject.sample - end - - context 'stats collected' do - before do - allow(stats).to receive(:active).and_return('active') - allow(stats).to receive(:queued).and_return('queued') - allow(raindrops).to receive(:unix_listener_stats).and_return({ socket_address => stats }) - end - - it 'updates metrics type unix and with addr' do - labels = { socket_type: 'unix', socket_address: socket_address } - - expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active') - expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued') - - subject.sample - end - end - end - - context 'unicorn listens on tcp sockets' do - let(:tcp_socket_address) { '0.0.0.0:8080' } - let(:tcp_sockets) { [tcp_socket_address] } - - before do - allow(unicorn).to receive(:listener_names).and_return(tcp_sockets) - end - - it 'samples socket data' do - expect(raindrops).to receive(:tcp_listener_stats).with(tcp_sockets) - - subject.sample - end - - context 'stats collected' do - before do - allow(stats).to receive(:active).and_return('active') - allow(stats).to receive(:queued).and_return('queued') - allow(raindrops).to receive(:tcp_listener_stats).and_return({ tcp_socket_address => stats }) - end - - it 'updates metrics type unix and with addr' do - labels = { socket_type: 'tcp', socket_address: tcp_socket_address } - - expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active') - expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued') - - subject.sample - end - end - end - - context 'unicorn workers' do - before do - allow(unicorn).to receive(:listener_names).and_return([]) - end - - context 'without http server' do - it "does set unicorn_workers to 0" do - expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, 0) - - subject.sample - end - end - - context 'with http server' do - let(:http_server_class) { Struct.new(:worker_processes) } - let!(:http_server) { http_server_class.new(5) } - - before do - stub_const('Unicorn::HttpServer', http_server_class) - end - - it "sets additional metrics" do - expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, 5) - - subject.sample - end - end - end - end - - describe '#start' do - context 'when enabled' do - before do - allow(subject).to receive(:enabled?).and_return(true) - end - - it 'creates new thread' do - expect(Thread).to receive(:new) - - subject.start - end - end - - context 'when disabled' do - before do - allow(subject).to receive(:enabled?).and_return(false) - end - - it "doesn't create new thread" do - expect(Thread).not_to receive(:new) - - subject.start - end - end - end -end diff --git a/spec/lib/gitlab/pagination/keyset/paginator_spec.rb b/spec/lib/gitlab/pagination/keyset/paginator_spec.rb new file mode 100644 index 00000000000..3c9a8913876 --- /dev/null +++ b/spec/lib/gitlab/pagination/keyset/paginator_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Pagination::Keyset::Paginator do + let_it_be(:project_1) { create(:project, created_at: 10.weeks.ago) } + let_it_be(:project_2) { create(:project, created_at: 2.weeks.ago) } + let_it_be(:project_3) { create(:project, created_at: 3.weeks.ago) } + let_it_be(:project_4) { create(:project, created_at: 5.weeks.ago) } + let_it_be(:project_5) { create(:project, created_at: 2.weeks.ago) } + + describe 'pagination' do + let(:per_page) { 10 } + let(:cursor) { nil } + let(:scope) { Project.order(created_at: :asc, id: :asc) } + let(:expected_order) { [project_1, project_4, project_3, project_2, project_5] } + + subject(:paginator) { scope.keyset_paginate(cursor: cursor, per_page: per_page) } + + context 'when per_page is greater than the record count' do + it { expect(paginator.records).to eq(expected_order) } + it { is_expected.not_to have_next_page } + it { is_expected.not_to have_previous_page } + + it 'has no next and previous cursor values' do + expect(paginator.cursor_for_next_page).to be_nil + expect(paginator.cursor_for_previous_page).to be_nil + end + end + + context 'when 0 records are returned' do + let(:scope) { Project.where(id: non_existing_record_id).order(created_at: :asc, id: :asc) } + + it { expect(paginator.records).to be_empty } + it { is_expected.not_to have_next_page } + it { is_expected.not_to have_previous_page } + end + + context 'when page size is smaller than the record count' do + let(:per_page) { 2 } + + it { expect(paginator.records).to eq(expected_order.first(2)) } + it { is_expected.to have_next_page } + it { is_expected.not_to have_previous_page } + + it 'has next page cursor' do + expect(paginator.cursor_for_next_page).not_to be_nil + end + + it 'does not have previous page cursor' do + expect(paginator.cursor_for_previous_page).to be_nil + end + + context 'when on the second page' do + let(:cursor) { scope.keyset_paginate(per_page: per_page).cursor_for_next_page } + + it { expect(paginator.records).to eq(expected_order[2...4]) } + it { is_expected.to have_next_page } + it { is_expected.to have_previous_page } + + context 'and then going back to the first page' do + let(:previous_page_cursor) { scope.keyset_paginate(cursor: cursor, per_page: per_page).cursor_for_previous_page } + + subject(:paginator) { scope.keyset_paginate(cursor: previous_page_cursor, per_page: per_page) } + + it { expect(paginator.records).to eq(expected_order.first(2)) } + it { is_expected.to have_next_page } + it { is_expected.not_to have_previous_page } + end + end + + context 'when jumping to the last page' do + let(:cursor) { scope.keyset_paginate(per_page: per_page).cursor_for_last_page } + + it { expect(paginator.records).to eq(expected_order.last(2)) } + it { is_expected.not_to have_next_page } + it { is_expected.to have_previous_page } + + context 'when paginating backwards' do + let(:previous_page_cursor) { scope.keyset_paginate(cursor: cursor, per_page: per_page).cursor_for_previous_page } + + subject(:paginator) { scope.keyset_paginate(cursor: previous_page_cursor, per_page: per_page) } + + it { expect(paginator.records).to eq(expected_order[-4...-2]) } + it { is_expected.to have_next_page } + it { is_expected.to have_previous_page } + end + + context 'when jumping to the first page' do + let(:first_page_cursor) { scope.keyset_paginate(cursor: cursor, per_page: per_page).cursor_for_first_page } + + subject(:paginator) { scope.keyset_paginate(cursor: first_page_cursor, per_page: per_page) } + + it { expect(paginator.records).to eq(expected_order.first(2)) } + it { is_expected.to have_next_page } + it { is_expected.not_to have_previous_page } + end + end + end + + describe 'default keyset direction parameter' do + let(:cursor_converter_class) { Gitlab::Pagination::Keyset::Paginator::Base64CursorConverter } + let(:per_page) { 2 } + + it 'exposes the direction parameter in the cursor' do + cursor = paginator.cursor_for_next_page + + expect(cursor_converter_class.parse(cursor)[:_kd]).to eq(described_class::FORWARD_DIRECTION) + end + end + end + + context 'when unsupported order is given' do + it 'raises error' do + scope = Project.order(path: :asc, name: :asc, id: :desc) # Cannot build 3 column order automatically + + expect { scope.keyset_paginate }.to raise_error(/does not support keyset pagination/) + end + end +end diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb index 88149465232..d811f67d16b 100644 --- a/spec/models/hooks/project_hook_spec.rb +++ b/spec/models/hooks/project_hook_spec.rb @@ -39,4 +39,15 @@ RSpec.describe ProjectHook do expect(hook.rate_limit).to be(100) end end + + describe '#application_context' do + let_it_be(:hook) { build(:project_hook) } + + it 'includes the type and project' do + expect(hook.application_context).to include( + related_class: 'ProjectHook', + project: hook.project + ) + end + end end diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb index 651716c3280..4ce2e729d89 100644 --- a/spec/models/hooks/service_hook_spec.rb +++ b/spec/models/hooks/service_hook_spec.rb @@ -30,4 +30,14 @@ RSpec.describe ServiceHook do expect(hook.rate_limit).to be_nil end end + + describe '#application_context' do + let(:hook) { build(:service_hook) } + + it 'includes the type' do + expect(hook.application_context).to eq( + related_class: 'ServiceHook' + ) + end + end end diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index a72034f1ac5..a99263078b3 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -177,4 +177,14 @@ RSpec.describe SystemHook do expect(hook.rate_limit).to be_nil end end + + describe '#application_context' do + let(:hook) { build(:system_hook) } + + it 'includes the type' do + expect(hook.application_context).to eq( + related_class: 'SystemHook' + ) + end + end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index a38ba782c44..009c8bd795e 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -8,8 +8,8 @@ RSpec.describe API::Branches do let(:guest) { create(:user).tap { |u| project.add_guest(u) } } let(:branch_name) { 'feature' } let(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } - let(:branch_with_dot) { project.repository.find_branch('ends-with.json') } - let(:branch_with_slash) { project.repository.find_branch('improve/awesome') } + let(:branch_with_dot) { 'ends-with.json' } + let(:branch_with_slash) { 'improve/awesome' } let(:project_id) { project.id } let(:current_user) { nil } @@ -285,6 +285,13 @@ RSpec.describe API::Branches do let(:request) { get api(route, current_user) } end end + + context 'when repository does not exist' do + it_behaves_like '404 response' do + let(:project) { create(:project, creator: user) } + let(:request) { get api(route, current_user) } + end + end end context 'when unauthenticated', 'and project is public' do @@ -320,19 +327,19 @@ RSpec.describe API::Branches do end context 'when branch contains a dot' do - let(:branch_name) { branch_with_dot.name } + let(:branch_name) { branch_with_dot } it_behaves_like 'repository branch' end context 'when branch contains dot txt' do - let(:branch_name) { project.repository.find_branch('ends-with.txt').name } + let(:branch_name) { 'ends-with.txt' } it_behaves_like 'repository branch' end context 'when branch contains a slash' do - let(:branch_name) { branch_with_slash.name } + let(:branch_name) { branch_with_slash } it_behaves_like '404 response' do let(:request) { get api(route, current_user) } @@ -340,7 +347,7 @@ RSpec.describe API::Branches do end context 'when branch contains an escaped slash' do - let(:branch_name) { CGI.escape(branch_with_slash.name) } + let(:branch_name) { CGI.escape(branch_with_slash) } it_behaves_like 'repository branch' end @@ -351,7 +358,7 @@ RSpec.describe API::Branches do it_behaves_like 'repository branch' context 'when branch contains a dot' do - let(:branch_name) { branch_with_dot.name } + let(:branch_name) { branch_with_dot } it_behaves_like 'repository branch' end @@ -475,13 +482,13 @@ RSpec.describe API::Branches do it_behaves_like 'repository new protected branch' context 'when branch contains a dot' do - let(:branch_name) { branch_with_dot.name } + let(:branch_name) { branch_with_dot } it_behaves_like 'repository new protected branch' end context 'when branch contains a slash' do - let(:branch_name) { branch_with_slash.name } + let(:branch_name) { branch_with_slash } it_behaves_like '404 response' do let(:request) { put api(route, current_user) } @@ -489,7 +496,7 @@ RSpec.describe API::Branches do end context 'when branch contains an escaped slash' do - let(:branch_name) { CGI.escape(branch_with_slash.name) } + let(:branch_name) { CGI.escape(branch_with_slash) } it_behaves_like 'repository new protected branch' end @@ -500,7 +507,7 @@ RSpec.describe API::Branches do it_behaves_like 'repository new protected branch' context 'when branch contains a dot' do - let(:branch_name) { branch_with_dot.name } + let(:branch_name) { branch_with_dot } it_behaves_like 'repository new protected branch' end @@ -609,13 +616,13 @@ RSpec.describe API::Branches do it_behaves_like 'repository unprotected branch' context 'when branch contains a dot' do - let(:branch_name) { branch_with_dot.name } + let(:branch_name) { branch_with_dot } it_behaves_like 'repository unprotected branch' end context 'when branch contains a slash' do - let(:branch_name) { branch_with_slash.name } + let(:branch_name) { branch_with_slash } it_behaves_like '404 response' do let(:request) { put api(route, current_user) } @@ -623,7 +630,7 @@ RSpec.describe API::Branches do end context 'when branch contains an escaped slash' do - let(:branch_name) { CGI.escape(branch_with_slash.name) } + let(:branch_name) { CGI.escape(branch_with_slash) } it_behaves_like 'repository unprotected branch' end @@ -634,7 +641,7 @@ RSpec.describe API::Branches do it_behaves_like 'repository unprotected branch' context 'when branch contains a dot' do - let(:branch_name) { branch_with_dot.name } + let(:branch_name) { branch_with_dot } it_behaves_like 'repository unprotected branch' end @@ -732,7 +739,7 @@ RSpec.describe API::Branches do end it 'removes a branch with dots in the branch name' do - delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}", user) + delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot}", user) expect(response).to have_gitlab_http_status(:no_content) end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 056f4d30ea5..43d7c31d87f 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -202,6 +202,16 @@ RSpec.describe 'project routing' do namespace_id: 'gitlab', project_id: 'gitlabhq', id: "stable", path: "new\n\nline.txt" }) end + + it_behaves_like 'redirecting a legacy path', '/gitlab/gitlabhq/refs/switch', '/gitlab/gitlabhq/-/refs/switch' + + it_behaves_like 'redirecting a legacy path', + '/gitlab/gitlabhq/refs/feature%2345/logs_tree', + '/gitlab/gitlabhq/-/refs/feature%2345/logs_tree' + + it_behaves_like 'redirecting a legacy path', + '/gitlab/gitlabhq/refs/stable/logs_tree/new%0A%0Aline.txt', + '/gitlab/gitlabhq/-/refs/stable/logs_tree/new%0A%0Aline.txt' end describe Projects::MergeRequestsController, 'routing' do @@ -357,9 +367,7 @@ RSpec.describe 'project routing' do expect(get('/gitlab/gitlabhq/-/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') end - it 'to #show unscoped routing' do - expect(get('/gitlab/gitlabhq/commit/4246fbd')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd') - end + it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/commit/4246fbd", "/gitlab/gitlabhq/-/commit/4246fbd" end # patch_project_commit GET /:project_id/commits/:id/patch(.:format) commits#patch @@ -376,9 +384,7 @@ RSpec.describe 'project routing' do expect(get('/gitlab/gitlabhq/-/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.atom') end - it 'to #show unscoped routing' do - expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.atom') - end + it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/commits/master", "/gitlab/gitlabhq/-/commits/master" end # project_project_members GET /:project_id/project_members(.:format) project_members#index @@ -517,6 +523,7 @@ RSpec.describe 'project routing' do end it 'to #show from unscoped routing' do + expect(get('/gitlab/gitlabhq/tree/master')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') expect(get('/gitlab/gitlabhq/tree/master/app/models/project.rb')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb') end end @@ -545,6 +552,9 @@ RSpec.describe 'project routing' do namespace_id: 'gitlab', project_id: 'gitlabhq', id: "#{newline_file}" }) end + + it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/find_file", "/gitlab/gitlabhq/-/find_file" + it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/files/master", "/gitlab/gitlabhq/-/files/master" end describe Projects::BlobController, 'routing' do @@ -575,6 +585,9 @@ RSpec.describe 'project routing' do namespace_id: 'gitlab', project_id: 'gitlabhq', id: "master/docs/#{newline_file}" }) end + + it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/new/master", "/gitlab/gitlabhq/-/new/master" + it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/edit/master/README", "/gitlab/gitlabhq/-/edit/master/README" end # project_raw GET /:project_id/-/raw/:id(.:format) raw#show {id: /[^\0]+/, project_id: /[^\/]+/} @@ -610,6 +623,9 @@ RSpec.describe 'project routing' do expect(get('/gitlab/gitlabhq/-/compare/master...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'master', to: 'stable') expect(get('/gitlab/gitlabhq/-/compare/issue/1234...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable') end + + it_behaves_like 'redirecting a legacy path', '/gitlab/gitlabhq/compare', '/gitlab/gitlabhq/-/compare' + it_behaves_like 'redirecting a legacy path', '/gitlab/gitlabhq/compare/master...stable', '/gitlab/gitlabhq/-/compare/master...stable' end describe Projects::NetworkController, 'routing' do diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb index e1954c32227..4ec8922e187 100644 --- a/spec/services/web_hook_service_spec.rb +++ b/spec/services/web_hook_service_spec.rb @@ -429,5 +429,19 @@ RSpec.describe WebHookService do end end end + + context 'when hook has custom context attributes' do + it 'includes the attributes in the worker context' do + expect(WebHookWorker).to receive(:perform_async) do + expect(Gitlab::ApplicationContext.current).to include( + 'meta.project' => project_hook.project.full_path, + 'meta.root_namespace' => project.root_ancestor.path, + 'meta.related_class' => 'ProjectHook' + ) + end + + service_instance.async_execute + end + end end end