Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-02-01 12:09:03 +00:00
parent 6e495b4e91
commit 9ecdb93f4e
130 changed files with 2015 additions and 1055 deletions

View file

@ -477,7 +477,7 @@ gem 'flipper', '~> 0.17.1'
gem 'flipper-active_record', '~> 0.17.1'
gem 'flipper-active_support_cache_store', '~> 0.17.1'
gem 'unleash', '~> 0.1.5'
gem 'gitlab-experiment', '~> 0.4.5'
gem 'gitlab-experiment', '~> 0.4.8'
# Structured logging
gem 'lograge', '~> 0.5'

View file

@ -424,7 +424,7 @@ GEM
github-markup (1.7.0)
gitlab-chronic (0.10.5)
numerizer (~> 0.2)
gitlab-experiment (0.4.5)
gitlab-experiment (0.4.8)
activesupport (>= 3.0)
scientist (~> 1.5, >= 1.5.0)
gitlab-fog-azure-rm (1.0.0)
@ -1364,7 +1364,7 @@ DEPENDENCIES
gitaly (~> 13.8.0.pre.rc3)
github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5)
gitlab-experiment (~> 0.4.5)
gitlab-experiment (~> 0.4.8)
gitlab-fog-azure-rm (~> 1.0)
gitlab-labkit (= 0.14.0)
gitlab-license (~> 1.0)

View file

@ -59,8 +59,7 @@ class Projects::IssuesController < Projects::ApplicationController
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
before_action :run_null_hypothesis_experiment,
only: [:index, :new, :create],
if: -> { Feature.enabled?(:gitlab_experiments) }
only: [:index, :new, :create]
respond_to :html

View file

@ -67,6 +67,9 @@ class SearchController < ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
def opensearch
end
private
# overridden in EE

View file

@ -1,13 +1,20 @@
# frozen_string_literal: true
class ApplicationExperiment < Gitlab::Experiment
def enabled?
return false if Feature::Definition.get(name).nil? # there has to be a feature flag yaml file
return false unless Gitlab.dev_env_or_com? # we're in an environment that allows experiments
Feature.get(name).state != :off # rubocop:disable Gitlab/AvoidFeatureGet
end
def publish(_result)
track(:assignment) # track that we've assigned a variant for this context
Gon.global.push({ experiment: { name => signature } }, true) # push to client
end
def track(action, **event_args)
return if excluded? # no events for opted out actors or excluded subjects
return unless should_track? # no events for opted out actors or excluded subjects
Gitlab::Tracking.event(name, action.to_s, **event_args.merge(
context: (event_args[:context] || []) << SnowplowTracker::SelfDescribingJson.new(

View file

@ -127,7 +127,7 @@ module Projects
access_level: group_access_level)
end
if Feature.enabled?(:specialized_project_authorization_workers)
if Feature.enabled?(:specialized_project_authorization_workers, default_enabled: :yaml)
AuthorizedProjectUpdate::ProjectCreateWorker.perform_async(@project.id)
# AuthorizedProjectsWorker uses an exclusive lease per user but
# specialized workers might have synchronization issues. Until we

View file

@ -79,6 +79,9 @@
= favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152'
%link{ rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)' }
-# OpenSearch
%link{ href: search_opensearch_path(format: :xml), rel: 'search', title: 'Search GitLab', type: 'application/opensearchdescription+xml' }
-# Windows 8 pinned site tile
%meta{ name: 'msapplication-TileImage', content: image_path('msapplication-tile.png') }
%meta{ name: 'msapplication-TileColor', content: '#30353E' }

View file

@ -0,0 +1,9 @@
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>GitLab</ShortName>
<Description>Search GitLab</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/x-icon"><%= root_url %>favicon.ico</Image>
<Url type="text/html" method="get" template="<%= search_url %>?search={searchTerms}"/>
<moz:SearchForm><%= search_url %></moz:SearchForm>
</OpenSearchDescription>

View file

@ -1,7 +1,7 @@
- type = local_assigns.fetch(:type)
- bulk_issue_health_status_flag = type == :issues && @project&.group&.feature_available?(:issuable_health_status)
- epic_bulk_edit_flag = @project&.group&.feature_available?(:epics) && type == :issues
- bulk_iterations_flag = @project.feature_available?(:iterations) && @project&.group.present? && type == :issues
- bulk_iterations_flag = @project&.group&.feature_available?(:iterations) && type == :issues
%aside.issues-bulk-update.js-right-sidebar.right-sidebar{ "aria-live" => "polite", data: { 'signed-in': current_user.present? } }
.issuable-sidebar.hidden

View file

@ -0,0 +1,5 @@
---
title: Escaped markdown should not be interpreted as shortcuts
merge_request: 45922
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Improve highlighting for merge diffs
merge_request: 52499
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Add job to persist On-call shifts
merge_request: 50239
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Expose if user is a bot in the REST api
merge_request: 52003
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Add OpenSearch support
merge_request: 52583
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Project creation to use specialized worker to calculate project authorizations
merge_request: 52719
author:
type: performance

View file

@ -0,0 +1,5 @@
---
title: Make LifecycleEvents exceptions to be fatal
merge_request: 52881
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Monitor RackAttack redis usage and enrich auth structured logs
merge_request: 52471
author:
type: changed

View file

@ -1,8 +0,0 @@
---
name: gitlab_experiments
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45840
rollout_issue_url:
milestone: '13.7'
type: development
group: group::adoption
default_enabled: false

View file

@ -0,0 +1,8 @@
---
name: honor_escaped_markdown
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45922
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300531
milestone: '13.9'
type: development
group: 'group::project management'
default_enabled: false

View file

@ -0,0 +1,8 @@
---
name: improved_merge_diff_highlighting
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52499
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299884
milestone: '13.9'
type: development
group: group::source code
default_enabled: false

View file

@ -1,8 +1,8 @@
---
name: specialized_project_authorization_workers
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31377
rollout_issue_url:
rollout_issue_url:
milestone: '13.0'
type: development
group: group::access
default_enabled: false
default_enabled: true

View file

@ -600,6 +600,9 @@ Gitlab.ee do
Settings.cron_jobs['incident_sla_exceeded_check_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['incident_sla_exceeded_check_worker']['cron'] ||= '*/2 * * * *'
Settings.cron_jobs['incident_sla_exceeded_check_worker']['job_class'] = 'IncidentManagement::IncidentSlaExceededCheckWorker'
Settings.cron_jobs['incident_management_persist_oncall_rotation_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['incident_management_persist_oncall_rotation_worker']['cron'] ||= '*/5 * * * *'
Settings.cron_jobs['incident_management_persist_oncall_rotation_worker']['job_class'] = 'IncidentManagement::OncallRotations::PersistAllRotationsShiftsJob'
Settings.cron_jobs['import_software_licenses_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['import_software_licenses_worker']['cron'] ||= '0 3 * * 0'
Settings.cron_jobs['import_software_licenses_worker']['job_class'] = 'ImportSoftwareLicensesWorker'

View file

@ -1,38 +0,0 @@
# frozen_string_literal: true
#
# Adds logging for all Rack Attack blocks and throttling events.
ActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish, request_id, payload|
req = payload[:request]
case req.env['rack.attack.match_type']
when :throttle, :blocklist, :track
rack_attack_info = {
message: 'Rack_Attack',
env: req.env['rack.attack.match_type'],
remote_ip: req.ip,
request_method: req.request_method,
path: req.fullpath,
matched: req.env['rack.attack.matched']
}
throttles_with_user_information = [
:throttle_authenticated_api,
:throttle_authenticated_web,
:throttle_authenticated_protected_paths_api,
:throttle_authenticated_protected_paths_web
]
if throttles_with_user_information.include? req.env['rack.attack.matched'].to_sym
user_id = req.env['rack.attack.match_discriminator']
user = User.find_by(id: user_id)
rack_attack_info[:user_id] = user_id
rack_attack_info['meta.user'] = user.username unless user.nil?
end
Gitlab::AuthLogger.error(rack_attack_info)
when :safelist
Gitlab::Instrumentation::Throttle.safelist = req.env['rack.attack.matched']
end
end

View file

@ -60,9 +60,10 @@ Rails.application.routes.draw do
end
# Search
get 'search' => 'search#show'
get 'search' => 'search#show', as: :search
get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
get 'search/count' => 'search#count', as: :search_count
get 'search/opensearch' => 'search#opensearch', as: :search_opensearch
# JSON Web Token
get 'jwt/auth' => 'jwt#auth'

View file

@ -168,6 +168,8 @@
- 2
- - incident_management_apply_incident_sla_exceeded_label
- 1
- - incident_management_oncall_rotations_persist_shifts_job
- 1
- - invalid_gpg_signature_update
- 2
- - irker

View file

@ -27,6 +27,7 @@ exceptions:
- CNA
- CNAME
- CORE
- CVS
- FREE
- CPU
- CRIME

View file

@ -264,6 +264,7 @@ Parameters:
"created_at": "2012-05-23T08:00:58Z",
"bio": "",
"bio_html": "",
"bot": false,
"location": null,
"public_email": "john@example.com",
"skype": "",

View file

@ -1,8 +0,0 @@
---
redirect_to: '../../topics/autodevops/stages.md#auto-deploy'
---
This document was moved to [another location](../../topics/autodevops/stages.md#auto-deploy).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../../topics/autodevops/stages.md#auto-deploy'
---
This document was moved to [another location](../../topics/autodevops/stages.md#auto-deploy).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: 'environments/index.md'
---
This document was moved to [another location](environments/index.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../../user/project/merge_requests/browser_performance_testing.md#configuring-browser-performance-testing'
---
This document was moved to [another location](../../user/project/merge_requests/browser_performance_testing.md#configuring-browser-performance-testing).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: 'code_quality.md'
---
This document was moved to [another location](code_quality.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../../user/project/merge_requests/code_quality.md#example-configuration'
---
This document was moved to [another location](../../user/project/merge_requests/code_quality.md#example-configuration).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../../user/application_security/container_scanning/index.md'
---
This document was moved to [another location](../../user/application_security/container_scanning/index.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../../user/application_security/dast/index.md'
---
This document was moved to [another location](../../user/application_security/dast/index.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../../user/application_security/dependency_scanning/index.md'
---
This document was moved to [another location](../../user/application_security/dependency_scanning/index.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../../user/compliance/license_compliance/index.md'
---
This document was moved to [another location](../../user/compliance/license_compliance/index.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../../user/application_security/sast/index.md'
---
This document was moved to [another location](../../user/application_security/sast/index.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../../user/application_security/container_scanning/index.md'
---
This document was moved to [another location](../../user/application_security/container_scanning/index.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: 'README.md'
---
This example is no longer available. [View other examples](README.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../migration/jenkins.md'
---
This document was moved to [another location](../migration/jenkins.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: 'unit_test_reports.md'
---
This document was moved to [unit_test_reports](unit_test_reports.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: 'multi_project_pipelines.md'
---
This document was moved to [another location](multi_project_pipelines.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: 'pipelines/index.md'
---
This document was moved to [another location](pipelines/index.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: 'README.md'
---
This document was moved to [another location](README.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../user/admin_area/appearance.md#sign-in--sign-up-pages'
---
This document was moved to [another location](../user/admin_area/appearance.md#sign-in--sign-up-pages).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../user/admin_area/appearance.md#navigation-bar'
---
This document was moved to [another location](../user/admin_area/appearance.md#navigation-bar).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../user/admin_area/appearance.md#favicon'
---
This document was moved to [another location](../user/admin_area/appearance.md#favicon).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../user/admin_area/settings/help_page.md'
---
This document was moved to [another location](../user/admin_area/settings/help_page.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../user/admin_area/appearance.md'
---
This document was moved to [another location](../user/admin_area/appearance.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../user/project/description_templates.md'
---
This document was moved to [description_templates](../user/project/description_templates.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../user/project/issues/managing_issues.md#closing-issues-automatically'
---
This document was moved to [another location](../user/project/issues/managing_issues.md#closing-issues-automatically).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../administration/libravatar.md'
---
This document was moved to [another location](../administration/libravatar.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../user/admin_area/appearance.md#new-project-pages'
---
This document was moved to [another location](../user/admin_area/appearance.md#new-project-pages).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../user/admin_area/appearance.md#system-header-and-footer-messages'
---
This document was moved to [another location](../user/admin_area/appearance.md#system-header-and-footer-messages).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../user/admin_area/appearance.md#sign-in--sign-up-pages'
---
This document was moved to [another location](../user/admin_area/appearance.md#sign-in--sign-up-pages).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: 'installation.md#google-protobuf-loaderror-libx86_64-linux-gnulibcso6-version-glibc_214-not-found'
---
This document was moved to [another location](installation.md#google-protobuf-loaderror-libx86_64-linux-gnulibcso6-version-glibc_214-not-found).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../administration/auth/ldap/index.md'
---
This document was moved to [another location](../administration/auth/ldap/index.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: installation.md#7-redis
---
This document was moved to [another location](installation.md#7-redis).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../administration/operations/cleaning_up_redis_sessions.md'
---
This document was moved to [another location](../administration/operations/cleaning_up_redis_sessions.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: alerts.md
---
This document was moved to [another location](alerts.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: alert_integrations.md
---
This document was moved to [another location](alert_integrations.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../administration/operations/moving_repositories.md'
---
This document was moved to [another location](../administration/operations/moving_repositories.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../administration/operations/sidekiq_memory_killer.md'
---
This document was moved to [another location](../administration/operations/sidekiq_memory_killer.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../administration/operations/unicorn.md'
---
This document was moved to [another location](../administration/operations/unicorn.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: 'https://about.gitlab.com/handbook/product/product-intelligence-guide/'
---
This document was moved to [another location](https://about.gitlab.com/handbook/product/product-intelligence-guide/).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -1,8 +0,0 @@
---
redirect_to: '../development/snowplow.md'
---
This document was moved to [another location](../development/snowplow.md).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View file

@ -23,107 +23,28 @@ Rewriting repository history is a destructive operation. Make sure to back up yo
you begin. The best way back up a repository is to
[export the project](../settings/import_export.md#exporting-a-project-and-its-data).
NOTE:
Git LFS files can only be removed by an Administrator using a
[Rake task](../../../raketasks/cleanup.md). Removal of this limitation
[is planned](https://gitlab.com/gitlab-org/gitlab/-/issues/223621).
## Purge files from repository history and storage
## Purge files from repository history
To reduce the size of your repository in GitLab, you must remove references to large files from branches, tags, and
To reduce the size of your repository in GitLab, you must remove references to large files from branches, tags, *and*
other internal references (refs) that are automatically created by GitLab. These refs include:
- `refs/merge-requests/*` for merge requests.
- `refs/pipelines/*` for
[pipelines](../../../ci/troubleshooting.md#fatal-reference-is-not-a-tree-error).
- `refs/environments/*` for environments.
- `refs/keep-around/*` are created as hidden refs to prevent commits referenced in the database from being removed
Git doesn't usually download these refs to make cloning and fetch faster, but we can use the `--mirror` option to
download all the advertised refs.
These refs are not automatically downloaded and hidden refs are not advertised, but we can remove these refs using a project export.
1. [Install `git filter-repo`](https://github.com/newren/git-filter-repo/blob/main/INSTALL.md)
using a supported package manager or from source.
1. Clone a fresh copy of the repository using `--bare` and `--mirror`:
```shell
git clone --bare --mirror https://gitlab.example.com/my/project.git
```
1. Using `git filter-repo`, purge any files from the history of your repository.
To purge large files, the `--strip-blobs-bigger-than` option can be used:
```shell
git filter-repo --strip-blobs-bigger-than 10M
```
To purge large files stored using Git LFS, the `--blob--callback` option can
be used. The example below, uses the callback to read the file size from the
Git LFS pointer, and removes files larger than 10MB.
```shell
git filter-repo --blob-callback '
if blob.data.startswith(b"version https://git-lfs.github.com/spec/v1"):
size_in_bytes = int.from_bytes(blob.data[124:], byteorder="big")
if size_in_bytes > 10*1000:
blob.skip()
'
```
To purge specific large files by path, the `--path` and `--invert-paths` options can be combined:
```shell
git filter-repo --path path/to/big/file.m4v --invert-paths
```
See the
[`git filter-repo` documentation](https://htmlpreview.github.io/?https://github.com/newren/git-filter-repo/blob/docs/html/git-filter-repo.html#EXAMPLES)
for more examples and the complete documentation.
1. Force push your changes to overwrite all branches on GitLab:
```shell
git push origin --force 'refs/heads/*'
```
[Protected branches](../protected_branches.md) cause this to fail. To proceed, you must
remove branch protection, push, and then re-enable protected branches.
1. To remove large files from tagged releases, force push your changes to all tags on GitLab:
```shell
git push origin --force 'refs/tags/*'
```
[Protected tags](../protected_tags.md) cause this to fail. To proceed, you must remove tag
protection, push, and then re-enable protected tags.
1. To prevent dead links to commits that no longer exist, push the `refs/replace` created by `git filter-repo`.
```shell
git push origin --force 'refs/replace/*'
```
Refer to the Git [`replace`](https://git-scm.com/book/en/v2/Git-Tools-Replace) documentation for information on how this works.
1. Run a [repository cleanup](#repository-cleanup).
NOTE:
Project statistics are cached for performance. You may need to wait 5-10 minutes
to see a reduction in storage utilization.
## Purge files from GitLab storage
In addition to the refs mentioned above, GitLab also creates hidden `refs/keep-around/*`to prevent commits being deleted. Hidden refs are not advertised, which means we can't download them using Git, but these refs are included in a project export.
To purge files from GitLab storage:
To purge files from a GitLab repository:
1. [Install `git filter-repo`](https://github.com/newren/git-filter-repo/blob/main/INSTALL.md)
using a supported package manager or from source.
1. Generate a fresh [export from the
project](../settings/import_export.html#exporting-a-project-and-its-data) and download it.
This project export contains a backup copy of your repository *and* refs
we can use to purge files from your repository.
1. Decompress the backup using `tar`:
@ -134,7 +55,7 @@ To purge files from GitLab storage:
This contains a `project.bundle` file, which was created by
[`git bundle`](https://git-scm.com/docs/git-bundle).
1. Clone a fresh copy of the repository from the bundle:
1. Clone a fresh copy of the repository from the bundle using `--bare` and `--mirror` options:
```shell
git clone --bare --mirror /path/to/project.bundle
@ -149,7 +70,7 @@ To purge files from GitLab storage:
the previous run. You need this file from **every** run. Do the next step every time you run
`git filter-repo`.
To purge all large files, the `--strip-blobs-bigger-than` option can be used:
To purge all files larger than 10M, the `--strip-blobs-bigger-than` option can be used:
```shell
git filter-repo --strip-blobs-bigger-than 10M
@ -236,14 +157,14 @@ This:
- Runs `git gc --prune=30.minutes.ago` against the repository to remove unreferenced objects. Repacking your repository temporarily
causes the size of your repository to increase significantly, because the old pack files are not removed until the
new pack files have been created.
- Unlinks any unused LFS objects currently attached to your project, freeing up storage space.
- Unlinks any unused LFS objects attached to your project, freeing up storage space.
- Recalculates the size of your repository on disk.
GitLab sends an email notification with the recalculated repository size after the cleanup has completed.
If the repository size does not decrease, this may be caused by loose objects
being kept around because they were referenced in a Git operation that happened
in the last 30 minutes. Try re-running these steps once the repository has been
in the last 30 minutes. Try re-running these steps after the repository has been
dormant for at least 30 minutes.
When using repository cleanup, note:

View file

@ -6,6 +6,7 @@ module API
include UsersHelper
expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) }
expose :bio, :bio_html, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title
expose :bot?, as: :bot
expose :work_information do |user|
work_information(user)
end

View file

@ -0,0 +1,40 @@
# frozen_string_literal: true
module Banzai
module Filter
class MarkdownPostEscapeFilter < HTML::Pipeline::Filter
LITERAL_KEYWORD = MarkdownPreEscapeFilter::LITERAL_KEYWORD
LITERAL_REGEX = %r{#{LITERAL_KEYWORD}-(.*?)-#{LITERAL_KEYWORD}}.freeze
NOT_LITERAL_REGEX = %r{#{LITERAL_KEYWORD}-((%5C|\\).+?)-#{LITERAL_KEYWORD}}.freeze
SPAN_REGEX = %r{<span>(.*?)</span>}.freeze
def call
return doc unless result[:escaped_literals]
# For any literals that actually didn't get escape processed
# (for example in code blocks), remove the special sequence.
html.gsub!(NOT_LITERAL_REGEX, '\1')
# Replace any left over literal sequences with `span` so that our
# reference processing is short-circuited
html.gsub!(LITERAL_REGEX, '<span>\1</span>')
# Since literals are converted in links, we need to remove any surrounding `span`.
# Note: this could have been done in the renderer,
# Banzai::Renderer::CommonMark::HTML. However, we eventually want to use
# the built-in compiled renderer, rather than the ruby version, for speed.
# So let's do this work here.
doc.css('a').each do |node|
node.attributes['href'].value = node.attributes['href'].value.gsub(SPAN_REGEX, '\1') if node.attributes['href']
node.attributes['title'].value = node.attributes['title'].value.gsub(SPAN_REGEX, '\1') if node.attributes['title']
end
doc.css('code').each do |node|
node.attributes['lang'].value = node.attributes['lang'].value.gsub(SPAN_REGEX, '\1') if node.attributes['lang']
end
doc
end
end
end
end

View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
module Banzai
module Filter
# In order to allow a user to short-circuit our reference shortcuts
# (such as # or !), the user should be able to escape them, like \#.
# CommonMark supports this, however it removes all information about
# what was actually a literal. In order to short-circuit the reference,
# we must surround backslash escaped ASCII punctuation with a custom sequence.
# This way CommonMark will properly handle the backslash escaped chars
# but we will maintain knowledge (the sequence) that it was a literal.
#
# We need to surround the character, not just prefix it. It could
# get converted into an entity by CommonMark and we wouldn't know how many
# characters there are. The entire literal needs to be surrounded with
# a `span` tag, which short-circuits our reference processing.
#
# We can't use a custom HTML tag since we could be initially surrounding
# text in an href, and then CommonMark will not be able to parse links
# properly. So we use `cmliteral-` and `-cmliteral`
#
# https://spec.commonmark.org/0.29/#backslash-escapes
#
# This filter does the initial surrounding, and MarkdownPostEscapeFilter
# does the conversion into span tags.
class MarkdownPreEscapeFilter < HTML::Pipeline::TextFilter
ASCII_PUNCTUATION = %r{([\\][!"#$%&'()*+,-./:;<=>?@\[\\\]^_`{|}~])}.freeze
LITERAL_KEYWORD = 'cmliteral'
def call
return @text unless Feature.enabled?(:honor_escaped_markdown, context[:group] || context[:project]&.group)
@text.gsub(ASCII_PUNCTUATION) do |match|
# The majority of markdown does not have literals. If none
# are found, we can bypass the post filter
result[:escaped_literals] = true
"#{LITERAL_KEYWORD}-#{match}-#{LITERAL_KEYWORD}"
end
end
end
end
end

View file

@ -5,7 +5,9 @@ module Banzai
class PlainMarkdownPipeline < BasePipeline
def self.filters
FilterArray[
Filter::MarkdownFilter
Filter::MarkdownPreEscapeFilter,
Filter::MarkdownFilter,
Filter::MarkdownPostEscapeFilter
]
end
end

View file

@ -4,17 +4,22 @@ module BulkImports
module Common
module Extractors
class GraphqlExtractor
def initialize(query)
@query = query[:query]
def initialize(options = {})
@query = options[:query]
end
def extract(context)
client = graphql_client(context)
client.execute(
response = client.execute(
client.parse(query.to_s),
query.variables(context.entity)
).original_hash.deep_dup
BulkImports::Pipeline::ExtractedData.new(
data: response.dig(*query.data_path),
page_info: response.dig(*query.page_info_path)
)
end
private
@ -27,10 +32,6 @@ module BulkImports
token: context.configuration.access_token
)
end
def parsed_query
@parsed_query ||= graphql_client.parse(query.to_s)
end
end
end
end

View file

@ -1,23 +0,0 @@
# frozen_string_literal: true
module BulkImports
module Common
module Transformers
class HashKeyDigger
def initialize(options = {})
@key_path = options[:key_path]
end
def transform(_, data)
raise ArgumentError, "Given data must be a Hash" unless data.is_a?(Hash)
data.dig(*Array.wrap(key_path))
end
private
attr_reader :key_path
end
end
end
end

View file

@ -1,19 +0,0 @@
# frozen_string_literal: true
module BulkImports
module Common
module Transformers
class UnderscorifyKeysTransformer
def initialize(options = {})
@options = options
end
def transform(_, data)
data.deep_transform_keys do |key|
key.to_s.underscore
end
end
end
end
end
end

View file

@ -9,9 +9,11 @@ module BulkImports
def extract(context)
encoded_parent_path = ERB::Util.url_encode(context.entity.source_full_path)
http_client(context.entity.bulk_import.configuration)
response = http_client(context.entity.bulk_import.configuration)
.each_page(:get, "groups/#{encoded_parent_path}/subgroups")
.flat_map(&:itself)
BulkImports::Pipeline::ExtractedData.new(data: response)
end
private

View file

@ -12,18 +12,18 @@ module BulkImports
group(fullPath: $full_path) {
name
path
fullPath
full_path: fullPath
description
visibility
emailsDisabled
lfsEnabled
mentionsDisabled
projectCreationLevel
requestAccessEnabled
requireTwoFactorAuthentication
shareWithGroupLock
subgroupCreationLevel
twoFactorGracePeriod
emails_disabled: emailsDisabled
lfs_enabled: lfsEnabled
mentions_disabled: mentionsDisabled
project_creation_level: projectCreationLevel
request_access_enabled: requestAccessEnabled
require_two_factor_authentication: requireTwoFactorAuthentication
share_with_group_lock: shareWithGroupLock
subgroup_creation_level: subgroupCreationLevel
two_factor_grace_period: twoFactorGracePeriod
}
}
GRAPHQL
@ -32,6 +32,18 @@ module BulkImports
def variables(entity)
{ full_path: entity.source_full_path }
end
def base_path
%w[data group]
end
def data_path
base_path
end
def page_info_path
base_path << 'page_info'
end
end
end
end

View file

@ -32,6 +32,18 @@ module BulkImports
cursor: entity.next_page_for(:labels)
}
end
def base_path
%w[data group labels]
end
def data_path
base_path << 'nodes'
end
def page_info_path
base_path << 'page_info'
end
end
end
end

View file

@ -7,16 +7,7 @@ module BulkImports
def initialize(*); end
def load(context, data)
Array.wrap(data['nodes']).each do |entry|
Labels::CreateService.new(entry)
.execute(group: context.entity.group)
end
context.entity.update_tracker_for(
relation: :labels,
has_next_page: data.dig('page_info', 'has_next_page'),
next_page: data.dig('page_info', 'end_cursor')
)
Labels::CreateService.new(data).execute(group: context.entity.group)
end
end
end

View file

@ -10,8 +10,6 @@ module BulkImports
extractor Common::Extractors::GraphqlExtractor, query: Graphql::GetGroupQuery
transformer Common::Transformers::HashKeyDigger, key_path: %w[data group]
transformer Common::Transformers::UnderscorifyKeysTransformer
transformer Common::Transformers::ProhibitedAttributesTransformer
transformer Groups::Transformers::GroupAttributesTransformer

View file

@ -9,13 +9,18 @@ module BulkImports
extractor BulkImports::Common::Extractors::GraphqlExtractor,
query: BulkImports::Groups::Graphql::GetLabelsQuery
transformer BulkImports::Common::Transformers::HashKeyDigger, key_path: %w[data group labels]
transformer Common::Transformers::ProhibitedAttributesTransformer
loader BulkImports::Groups::Loaders::LabelsLoader
def after_run(context)
if context.entity.has_next_page?(:labels)
def after_run(context, extracted_data)
context.entity.update_tracker_for(
relation: :labels,
has_next_page: extracted_data.has_next_page?,
next_page: extracted_data.next_page
)
if extracted_data.has_next_page?
run(context)
end
end

View file

@ -0,0 +1,26 @@
# frozen_string_literal: true
module BulkImports
module Pipeline
class ExtractedData
attr_reader :data
def initialize(data: nil, page_info: {})
@data = Array.wrap(data)
@page_info = page_info
end
def has_next_page?
@page_info['has_next_page']
end
def next_page
@page_info['end_cursor']
end
def each(&block)
data.each(&block)
end
end
end
end

View file

@ -12,7 +12,9 @@ module BulkImports
info(context, message: 'Pipeline started', pipeline_class: pipeline)
Array.wrap(extracted_data_from(context)).each do |entry|
extracted_data = extracted_data_from(context)
extracted_data&.each do |entry|
transformers.each do |transformer|
entry = run_pipeline_step(:transformer, transformer.class.name, context) do
transformer.transform(context, entry)
@ -24,7 +26,7 @@ module BulkImports
end
end
after_run(context) if respond_to?(:after_run)
after_run(context, extracted_data) if respond_to?(:after_run)
rescue MarkedAsFailedError
log_skip(context)
end
@ -43,6 +45,8 @@ module BulkImports
log_import_failure(e, step, context)
mark_as_failed(context) if abort_on_failure?
nil
end
def extracted_data_from(context)

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true
require_relative '../utils' # Gitlab::Utils
module Gitlab
module Cluster
#
@ -64,6 +66,10 @@ module Gitlab
# Blocks will be executed in the order in which they are registered.
#
class LifecycleEvents
FatalError = Class.new(Exception) # rubocop:disable Lint/InheritException
USE_FATAL_LIFECYCLE_EVENTS = Gitlab::Utils.to_boolean(ENV.fetch('GITLAB_FATAL_LIFECYCLE_EVENTS', 'true'))
class << self
#
# Hook registration methods (called from initializers)
@ -111,24 +117,24 @@ module Gitlab
# Lifecycle integration methods (called from unicorn.rb, puma.rb, etc.)
#
def do_worker_start
call(@worker_start_hooks)
call(:worker_start_hooks, @worker_start_hooks)
end
def do_before_fork
call(@before_fork_hooks)
call(:before_fork_hooks, @before_fork_hooks)
end
def do_before_graceful_shutdown
call(@master_blackout_period)
call(:master_blackout_period, @master_blackout_period)
blackout_seconds = ::Settings.shutdown.blackout_seconds.to_i
sleep(blackout_seconds) if blackout_seconds > 0
call(@master_graceful_shutdown)
call(:master_graceful_shutdown, @master_graceful_shutdown)
end
def do_before_master_restart
call(@master_restart_hooks)
call(:master_restart_hooks, @master_restart_hooks)
end
# DEPRECATED
@ -143,8 +149,18 @@ module Gitlab
private
def call(hooks)
hooks&.each(&:call)
def call(name, hooks)
return unless hooks
hooks.each do |hook|
hook.call
rescue => e
Gitlab::ErrorTracking.track_exception(e, type: 'LifecycleEvents', hook: hook)
warn("ERROR: The hook #{name} failed with exception (#{e.class}) \"#{e.message}\".")
# we consider lifecycle hooks to be fatal errors
raise FatalError, e if USE_FATAL_LIFECYCLE_EVENTS
end
end
def in_clustered_environment?

View file

@ -0,0 +1,74 @@
# frozen_string_literal: true
module Gitlab
module Diff
class CharDiff
include Gitlab::Utils::StrongMemoize
def initialize(old_string, new_string)
@old_string = old_string.to_s
@new_string = new_string.to_s
@changes = []
end
def generate_diff
@changes = diff_match_patch.diff_main(@old_string, @new_string)
diff_match_patch.diff_cleanupSemantic(@changes)
@changes
end
def changed_ranges(offset: 0)
old_diffs = []
new_diffs = []
new_pointer = old_pointer = offset
generate_diff.each do |(action, content)|
content_size = content.size
if action == :equal
new_pointer += content_size
old_pointer += content_size
end
if action == :delete
old_diffs << (old_pointer..(old_pointer + content_size - 1))
old_pointer += content_size
end
if action == :insert
new_diffs << (new_pointer..(new_pointer + content_size - 1))
new_pointer += content_size
end
end
[old_diffs, new_diffs]
end
def to_html
@changes.map do |op, text|
%{<span class="#{html_class_names(op)}">#{ERB::Util.html_escape(text)}</span>}
end.join.html_safe
end
private
def diff_match_patch
strong_memoize(:diff_match_patch) { DiffMatchPatch.new }
end
def html_class_names(operation)
class_names = ['idiff']
case operation
when :insert
class_names << 'addition'
when :delete
class_names << 'deletion'
end
class_names.join(' ')
end
end
end
end

View file

@ -3,12 +3,13 @@
module Gitlab
module Diff
class Highlight
attr_reader :diff_file, :diff_lines, :raw_lines, :repository
attr_reader :diff_file, :diff_lines, :raw_lines, :repository, :project
delegate :old_path, :new_path, :old_sha, :new_sha, to: :diff_file, prefix: :diff
def initialize(diff_lines, repository: nil)
@repository = repository
@project = repository&.project
if diff_lines.is_a?(Gitlab::Diff::File)
@diff_file = diff_lines
@ -66,7 +67,7 @@ module Gitlab
end
def inline_diffs
@inline_diffs ||= InlineDiff.for_lines(@raw_lines)
@inline_diffs ||= InlineDiff.for_lines(@raw_lines, project: project)
end
def old_lines

View file

@ -8,6 +8,7 @@ module Gitlab
EXPIRATION = 1.week
VERSION = 1
NEXT_VERSION = 2
delegate :diffable, to: :@diff_collection
delegate :diff_options, to: :@diff_collection
@ -69,12 +70,20 @@ module Gitlab
def key
strong_memoize(:redis_key) do
['highlighted-diff-files', diffable.cache_key, VERSION, diff_options].join(":")
['highlighted-diff-files', diffable.cache_key, version, diff_options].join(":")
end
end
private
def version
if Feature.enabled?(:improved_merge_diff_highlighting, diffable.project)
NEXT_VERSION
else
VERSION
end
end
def set_highlighted_diff_lines(diff_file, content)
diff_file.highlighted_diff_lines = content.map do |line|
Gitlab::Diff::Line.safe_init_from_hash(line)

View file

@ -27,28 +27,19 @@ module Gitlab
@offset = offset
end
def inline_diffs
def inline_diffs(project: nil)
# Skip inline diff if empty line was replaced with content
return if old_line == ""
lcp = longest_common_prefix(old_line, new_line)
lcs = longest_common_suffix(old_line[lcp..-1], new_line[lcp..-1])
lcp += offset
old_length = old_line.length + offset
new_length = new_line.length + offset
old_diff_range = lcp..(old_length - lcs - 1)
new_diff_range = lcp..(new_length - lcs - 1)
old_diffs = [old_diff_range] if old_diff_range.begin <= old_diff_range.end
new_diffs = [new_diff_range] if new_diff_range.begin <= new_diff_range.end
[old_diffs, new_diffs]
if Feature.enabled?(:improved_merge_diff_highlighting, project)
CharDiff.new(old_line, new_line).changed_ranges(offset: offset)
else
deprecated_diff
end
end
class << self
def for_lines(lines)
def for_lines(lines, project: nil)
changed_line_pairs = find_changed_line_pairs(lines)
inline_diffs = []
@ -57,7 +48,7 @@ module Gitlab
old_line = lines[old_index]
new_line = lines[new_index]
old_diffs, new_diffs = new(old_line, new_line, offset: 1).inline_diffs
old_diffs, new_diffs = new(old_line, new_line, offset: 1).inline_diffs(project: project)
inline_diffs[old_index] = old_diffs
inline_diffs[new_index] = new_diffs
@ -97,6 +88,24 @@ module Gitlab
private
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/299884
def deprecated_diff
lcp = longest_common_prefix(old_line, new_line)
lcs = longest_common_suffix(old_line[lcp..-1], new_line[lcp..-1])
lcp += offset
old_length = old_line.length + offset
new_length = new_line.length + offset
old_diff_range = lcp..(old_length - lcs - 1)
new_diff_range = lcp..(new_length - lcs - 1)
old_diffs = [old_diff_range] if old_diff_range.begin <= old_diff_range.end
new_diffs = [new_diff_range] if new_diff_range.begin <= new_diff_range.end
[old_diffs, new_diffs]
end
def longest_common_prefix(a, b) # rubocop:disable Naming/UncommunicativeMethodParamName
max_length = [a.length, b.length].max

View file

@ -15,7 +15,8 @@ module Gitlab
:elasticsearch_duration_s,
*::Gitlab::Instrumentation::Redis.known_payload_keys,
*::Gitlab::Metrics::Subscribers::ActiveRecord::DB_COUNTERS,
*::Gitlab::Metrics::Subscribers::ExternalHttp::KNOWN_PAYLOAD_KEYS]
*::Gitlab::Metrics::Subscribers::ExternalHttp::KNOWN_PAYLOAD_KEYS,
*::Gitlab::Metrics::Subscribers::RackAttack::PAYLOAD_KEYS]
end
def add_instrumentation_data(payload)
@ -26,6 +27,7 @@ module Gitlab
instrument_throttle(payload)
instrument_active_record(payload)
instrument_external_http(payload)
instrument_rack_attack(payload)
end
def instrument_gitaly(payload)
@ -80,6 +82,13 @@ module Gitlab
payload.merge!(db_counters)
end
def instrument_rack_attack(payload)
rack_attack_redis_count = ::Gitlab::Metrics::Subscribers::RackAttack.payload[:rack_attack_redis_count]
return if rack_attack_redis_count == 0
payload.merge!(::Gitlab::Metrics::Subscribers::RackAttack.payload)
end
# Returns the queuing duration for a Sidekiq job in seconds, as a float, if the
# `enqueued_at` field or `created_at` field is available.
#

View file

@ -0,0 +1,91 @@
# frozen_string_literal: true
module Gitlab
module Metrics
module Subscribers
# - Adds logging for all Rack Attack blocks and throttling events.
# - Instrument the cache operations of RackAttack to use in structured
# logs. Two fields are exposed:
# + rack_attack_redis_count: the number of redis calls triggered by
# RackAttack in a request.
# + rack_attack_redis_duration_s: the total duration of all redis calls
# triggered by RackAttack in a request.
class RackAttack < ActiveSupport::Subscriber
attach_to 'rack_attack'
INSTRUMENTATION_STORE_KEY = :rack_attack_instrumentation
THROTTLES_WITH_USER_INFORMATION = [
:throttle_authenticated_api,
:throttle_authenticated_web,
:throttle_authenticated_protected_paths_api,
:throttle_authenticated_protected_paths_web
].freeze
PAYLOAD_KEYS = [
:rack_attack_redis_count,
:rack_attack_redis_duration_s
].freeze
def self.payload
Gitlab::SafeRequestStore[INSTRUMENTATION_STORE_KEY] ||= {
rack_attack_redis_count: 0,
rack_attack_redis_duration_s: 0.0
}
end
def redis(event)
self.class.payload[:rack_attack_redis_count] += 1
self.class.payload[:rack_attack_redis_duration_s] += event.duration.to_f / 1000
end
def safelist(event)
req = event.payload[:request]
Gitlab::Instrumentation::Throttle.safelist = req.env['rack.attack.matched']
end
def throttle(event)
log_into_auth_logger(event)
end
def blocklist(event)
log_into_auth_logger(event)
end
def track(event)
log_into_auth_logger(event)
end
private
def log_into_auth_logger(event)
req = event.payload[:request]
rack_attack_info = {
message: 'Rack_Attack',
env: req.env['rack.attack.match_type'],
remote_ip: req.ip,
request_method: req.request_method,
path: req.fullpath,
matched: req.env['rack.attack.matched']
}
if THROTTLES_WITH_USER_INFORMATION.include? req.env['rack.attack.matched'].to_sym
user_id = req.env['rack.attack.match_discriminator']
user = User.find_by(id: user_id) # rubocop:disable CodeReuse/ActiveRecord
rack_attack_info[:user_id] = user_id
rack_attack_info['meta.user'] = user.username unless user.nil?
end
Gitlab::InstrumentationHelper.add_instrumentation_data(rack_attack_info)
logger.error(rack_attack_info)
end
def logger
Gitlab::AuthLogger
end
end
end
end
end

View file

@ -12,13 +12,15 @@ module Gitlab
rack_attack::Request.include(Gitlab::RackAttack::Request)
# This is Rack::Attack::DEFAULT_THROTTLED_RESPONSE, modified to allow a custom response
Rack::Attack.throttled_response = lambda do |env|
rack_attack.throttled_response = lambda do |env|
throttled_headers = Gitlab::RackAttack.throttled_response_headers(
env['rack.attack.matched'], env['rack.attack.match_data']
)
[429, { 'Content-Type' => 'text/plain' }.merge(throttled_headers), [Gitlab::Throttle.rate_limiting_response_text]]
end
rack_attack.cache.store = Gitlab::RackAttack::InstrumentedCacheStore.new
# Configure the throttles
configure_throttles(rack_attack)

View file

@ -0,0 +1,32 @@
# frozen_string_literal: true
module Gitlab
module RackAttack
# This class is a proxy for all Redis calls made by RackAttack. All the
# calls are instrumented, then redirected to ::Rails.cache. This class
# instruments the standard interfaces of ActiveRecord::Cache defined in
# https://github.com/rails/rails/blob/v6.0.3.1/activesupport/lib/active_support/cache.rb#L315
#
# For more information, please see
# https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/751
class InstrumentedCacheStore
NOTIFICATION_CHANNEL = 'redis.rack_attack'
delegate :silence!, :mute, to: :@upstream_store
def initialize(upstream_store: ::Rails.cache, notifier: ActiveSupport::Notifications)
@upstream_store = upstream_store
@notifier = notifier
end
[:fetch, :read, :read_multi, :write_multi, :fetch_multi, :write, :delete,
:exist?, :delete_matched, :increment, :decrement, :cleanup, :clear].each do |interface|
define_method interface do |*args, **k_args, &block|
@notifier.instrument(NOTIFICATION_CHANNEL, operation: interface) do
@upstream_store.public_send(interface, *args, **k_args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
end
end
end

View file

@ -0,0 +1,133 @@
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Aggregates
UNION_OF_AGGREGATED_METRICS = 'OR'
INTERSECTION_OF_AGGREGATED_METRICS = 'AND'
ALLOWED_METRICS_AGGREGATIONS = [UNION_OF_AGGREGATED_METRICS, INTERSECTION_OF_AGGREGATED_METRICS].freeze
AGGREGATED_METRICS_PATH = Rails.root.join('lib/gitlab/usage_data_counters/aggregated_metrics/*.yml')
UnknownAggregationOperator = Class.new(StandardError)
class Aggregate
delegate :calculate_events_union,
:weekly_time_range,
:monthly_time_range,
to: Gitlab::UsageDataCounters::HLLRedisCounter
def initialize
@aggregated_metrics = load_events(AGGREGATED_METRICS_PATH)
end
def monthly_data
aggregated_metrics_data(**monthly_time_range)
end
def weekly_data
aggregated_metrics_data(**weekly_time_range)
end
private
attr_accessor :aggregated_metrics
def aggregated_metrics_data(start_date:, end_date:)
aggregated_metrics.each_with_object({}) do |aggregation, weekly_data|
next if aggregation[:feature_flag] && Feature.disabled?(aggregation[:feature_flag], default_enabled: false, type: :development)
weekly_data[aggregation[:name]] = calculate_count_for_aggregation(aggregation, start_date: start_date, end_date: end_date)
end
end
def calculate_count_for_aggregation(aggregation, start_date:, end_date:)
case aggregation[:operator]
when UNION_OF_AGGREGATED_METRICS
calculate_events_union(event_names: aggregation[:events], start_date: start_date, end_date: end_date)
when INTERSECTION_OF_AGGREGATED_METRICS
calculate_events_intersections(event_names: aggregation[:events], start_date: start_date, end_date: end_date)
else
Gitlab::ErrorTracking
.track_and_raise_for_dev_exception(UnknownAggregationOperator.new("Events should be aggregated with one of operators #{ALLOWED_METRICS_AGGREGATIONS}"))
Gitlab::Utils::UsageData::FALLBACK
end
rescue Gitlab::UsageDataCounters::HLLRedisCounter::EventError => error
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(error)
Gitlab::Utils::UsageData::FALLBACK
end
# calculate intersection of 'n' sets based on inclusion exclusion principle https://en.wikipedia.org/wiki/Inclusion%E2%80%93exclusion_principle
# this method will be extracted to dedicated module with https://gitlab.com/gitlab-org/gitlab/-/issues/273391
def calculate_events_intersections(event_names:, start_date:, end_date:, subset_powers_cache: Hash.new({}))
# calculate power of intersection of all given metrics from inclusion exclusion principle
# |A + B + C| = (|A| + |B| + |C|) - (|A & B| + |A & C| + .. + |C & D|) + (|A & B & C|) =>
# |A & B & C| = - (|A| + |B| + |C|) + (|A & B| + |A & C| + .. + |C & D|) + |A + B + C|
# |A + B + C + D| = (|A| + |B| + |C| + |D|) - (|A & B| + |A & C| + .. + |C & D|) + (|A & B & C| + |B & C & D|) - |A & B & C & D| =>
# |A & B & C & D| = (|A| + |B| + |C| + |D|) - (|A & B| + |A & C| + .. + |C & D|) + (|A & B & C| + |B & C & D|) - |A + B + C + D|
# calculate each components of equation except for the last one |A & B & C & D| = (|A| + |B| + |C| + |D|) - (|A & B| + |A & C| + .. + |C & D|) + (|A & B & C| + |B & C & D|) - ...
subset_powers_data = subsets_intersection_powers(event_names, start_date, end_date, subset_powers_cache)
# calculate last component of the equation |A & B & C & D| = .... - |A + B + C + D|
power_of_union_of_all_events = begin
subset_powers_cache[event_names.size][event_names.join('_+_')] ||= \
calculate_events_union(event_names: event_names, start_date: start_date, end_date: end_date)
end
# in order to determine if part of equation (|A & B & C|, |A & B & C & D|), that represents the intersection that we need to calculate,
# is positive or negative in particular equation we need to determine if number of subsets is even or odd. Please take a look at two examples below
# |A + B + C| = (|A| + |B| + |C|) - (|A & B| + |A & C| + .. + |C & D|) + |A & B & C| =>
# |A & B & C| = - (|A| + |B| + |C|) + (|A & B| + |A & C| + .. + |C & D|) + |A + B + C|
# |A + B + C + D| = (|A| + |B| + |C| + |D|) - (|A & B| + |A & C| + .. + |C & D|) + (|A & B & C| + |B & C & D|) - |A & B & C & D| =>
# |A & B & C & D| = (|A| + |B| + |C| + |D|) - (|A & B| + |A & C| + .. + |C & D|) + (|A & B & C| + |B & C & D|) - |A + B + C + D|
subset_powers_size_even = subset_powers_data.size.even?
# sum all components of equation except for the last one |A & B & C & D| = (|A| + |B| + |C| + |D|) - (|A & B| + |A & C| + .. + |C & D|) + (|A & B & C| + |B & C & D|) - ... =>
sum_of_all_subset_powers = sum_subset_powers(subset_powers_data, subset_powers_size_even)
# add last component of the equation |A & B & C & D| = sum_of_all_subset_powers - |A + B + C + D|
sum_of_all_subset_powers + (subset_powers_size_even ? power_of_union_of_all_events : -power_of_union_of_all_events)
end
def sum_subset_powers(subset_powers_data, subset_powers_size_even)
sum_without_sign = subset_powers_data.to_enum.with_index.sum do |value, index|
(index + 1).odd? ? value : -value
end
(subset_powers_size_even ? -1 : 1) * sum_without_sign
end
def subsets_intersection_powers(event_names, start_date, end_date, subset_powers_cache)
subset_sizes = (1..(event_names.size - 1))
subset_sizes.map do |subset_size|
if subset_size > 1
# calculate sum of powers of intersection between each subset (with given size) of metrics: #|A + B + C + D| = ... - (|A & B| + |A & C| + .. + |C & D|)
event_names.combination(subset_size).sum do |events_subset|
subset_powers_cache[subset_size][events_subset.join('_&_')] ||= \
calculate_events_intersections(event_names: events_subset, start_date: start_date, end_date: end_date, subset_powers_cache: subset_powers_cache)
end
else
# calculate sum of powers of each set (metric) alone #|A + B + C + D| = (|A| + |B| + |C| + |D|) - ...
event_names.sum do |event|
subset_powers_cache[subset_size][event] ||= \
calculate_events_union(event_names: event, start_date: start_date, end_date: end_date)
end
end
end
end
def load_events(wildcard)
Dir[wildcard].each_with_object([]) do |path, events|
events.push(*load_yaml_from_path(path))
end
end
def load_yaml_from_path(path)
YAML.safe_load(File.read(path))&.map(&:with_indifferent_access)
end
end
end
end
end
end

View file

@ -23,6 +23,7 @@ module Gitlab
deployment_minimum_id
deployment_maximum_id
auth_providers
aggregated_metrics
recorded_at
).freeze
@ -691,13 +692,13 @@ module Gitlab
def aggregated_metrics_monthly
{
aggregated_metrics: ::Gitlab::UsageDataCounters::HLLRedisCounter.aggregated_metrics_monthly_data
aggregated_metrics: aggregated_metrics.monthly_data
}
end
def aggregated_metrics_weekly
{
aggregated_metrics: ::Gitlab::UsageDataCounters::HLLRedisCounter.aggregated_metrics_weekly_data
aggregated_metrics: aggregated_metrics.weekly_data
}
end
@ -742,6 +743,10 @@ module Gitlab
private
def aggregated_metrics
@aggregated_metrics ||= ::Gitlab::Usage::Metrics::Aggregates::Aggregate.new
end
def event_monthly_active_users(date_range)
data = {
action_monthly_active_users_project_repo: Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION,

View file

@ -13,15 +13,10 @@ module Gitlab
AggregationMismatch = Class.new(EventError)
SlotMismatch = Class.new(EventError)
CategoryMismatch = Class.new(EventError)
UnknownAggregationOperator = Class.new(EventError)
InvalidContext = Class.new(EventError)
KNOWN_EVENTS_PATH = File.expand_path('known_events/*.yml', __dir__)
ALLOWED_AGGREGATIONS = %i(daily weekly).freeze
UNION_OF_AGGREGATED_METRICS = 'OR'
INTERSECTION_OF_AGGREGATED_METRICS = 'AND'
ALLOWED_METRICS_AGGREGATIONS = [UNION_OF_AGGREGATED_METRICS, INTERSECTION_OF_AGGREGATED_METRICS].freeze
AGGREGATED_METRICS_PATH = File.expand_path('aggregated_metrics/*.yml', __dir__)
# Track event on entity_id
# Increment a Redis HLL counter for unique event_name and entity_id
@ -90,37 +85,40 @@ module Gitlab
events_names = events_for_category(category)
event_results = events_names.each_with_object({}) do |event, hash|
hash["#{event}_weekly"] = unique_events(event_names: [event], start_date: 7.days.ago.to_date, end_date: Date.current)
hash["#{event}_monthly"] = unique_events(event_names: [event], start_date: 4.weeks.ago.to_date, end_date: Date.current)
hash["#{event}_weekly"] = unique_events(**weekly_time_range.merge(event_names: [event]))
hash["#{event}_monthly"] = unique_events(**monthly_time_range.merge(event_names: [event]))
end
if eligible_for_totals?(events_names)
event_results["#{category}_total_unique_counts_weekly"] = unique_events(event_names: events_names, start_date: 7.days.ago.to_date, end_date: Date.current)
event_results["#{category}_total_unique_counts_monthly"] = unique_events(event_names: events_names, start_date: 4.weeks.ago.to_date, end_date: Date.current)
event_results["#{category}_total_unique_counts_weekly"] = unique_events(**weekly_time_range.merge(event_names: events_names))
event_results["#{category}_total_unique_counts_monthly"] = unique_events(**monthly_time_range.merge(event_names: events_names))
end
category_results["#{category}"] = event_results
end
end
def weekly_time_range
{ start_date: 7.days.ago.to_date, end_date: Date.current }
end
def monthly_time_range
{ start_date: 4.weeks.ago.to_date, end_date: Date.current }
end
def known_event?(event_name)
event_for(event_name).present?
end
def aggregated_metrics_monthly_data
aggregated_metrics_data(4.weeks.ago.to_date)
end
def aggregated_metrics_weekly_data
aggregated_metrics_data(7.days.ago.to_date)
end
def known_events
@known_events ||= load_events(KNOWN_EVENTS_PATH)
end
def aggregated_metrics
@aggregated_metrics ||= load_events(AGGREGATED_METRICS_PATH)
def calculate_events_union(event_names:, start_date:, end_date:)
count_unique_events(event_names: event_names, start_date: start_date, end_date: end_date) do |events|
raise SlotMismatch, events unless events_in_same_slot?(events)
raise AggregationMismatch, events unless events_same_aggregation?(events)
end
end
private
@ -139,93 +137,6 @@ module Gitlab
Plan.all_plans
end
def aggregated_metrics_data(start_date)
aggregated_metrics.each_with_object({}) do |aggregation, weekly_data|
next if aggregation[:feature_flag] && Feature.disabled?(aggregation[:feature_flag], default_enabled: false, type: :development)
weekly_data[aggregation[:name]] = calculate_count_for_aggregation(aggregation, start_date: start_date, end_date: Date.current)
end
end
def calculate_count_for_aggregation(aggregation, start_date:, end_date:)
case aggregation[:operator]
when UNION_OF_AGGREGATED_METRICS
calculate_events_union(event_names: aggregation[:events], start_date: start_date, end_date: end_date)
when INTERSECTION_OF_AGGREGATED_METRICS
calculate_events_intersections(event_names: aggregation[:events], start_date: start_date, end_date: end_date)
else
raise UnknownAggregationOperator, "Events should be aggregated with one of operators #{ALLOWED_METRICS_AGGREGATIONS}"
end
end
# calculate intersection of 'n' sets based on inclusion exclusion principle https://en.wikipedia.org/wiki/Inclusion%E2%80%93exclusion_principle
# this method will be extracted to dedicated module with https://gitlab.com/gitlab-org/gitlab/-/issues/273391
def calculate_events_intersections(event_names:, start_date:, end_date:, subset_powers_cache: Hash.new({}))
# calculate power of intersection of all given metrics from inclusion exclusion principle
# |A + B + C| = (|A| + |B| + |C|) - (|A & B| + |A & C| + .. + |C & D|) + (|A & B & C|) =>
# |A & B & C| = - (|A| + |B| + |C|) + (|A & B| + |A & C| + .. + |C & D|) + |A + B + C|
# |A + B + C + D| = (|A| + |B| + |C| + |D|) - (|A & B| + |A & C| + .. + |C & D|) + (|A & B & C| + |B & C & D|) - |A & B & C & D| =>
# |A & B & C & D| = (|A| + |B| + |C| + |D|) - (|A & B| + |A & C| + .. + |C & D|) + (|A & B & C| + |B & C & D|) - |A + B + C + D|
# calculate each components of equation except for the last one |A & B & C & D| = (|A| + |B| + |C| + |D|) - (|A & B| + |A & C| + .. + |C & D|) + (|A & B & C| + |B & C & D|) - ...
subset_powers_data = subsets_intersection_powers(event_names, start_date, end_date, subset_powers_cache)
# calculate last component of the equation |A & B & C & D| = .... - |A + B + C + D|
power_of_union_of_all_events = begin
subset_powers_cache[event_names.size][event_names.join('_+_')] ||= \
calculate_events_union(event_names: event_names, start_date: start_date, end_date: end_date)
end
# in order to determine if part of equation (|A & B & C|, |A & B & C & D|), that represents the intersection that we need to calculate,
# is positive or negative in particular equation we need to determine if number of subsets is even or odd. Please take a look at two examples below
# |A + B + C| = (|A| + |B| + |C|) - (|A & B| + |A & C| + .. + |C & D|) + |A & B & C| =>
# |A & B & C| = - (|A| + |B| + |C|) + (|A & B| + |A & C| + .. + |C & D|) + |A + B + C|
# |A + B + C + D| = (|A| + |B| + |C| + |D|) - (|A & B| + |A & C| + .. + |C & D|) + (|A & B & C| + |B & C & D|) - |A & B & C & D| =>
# |A & B & C & D| = (|A| + |B| + |C| + |D|) - (|A & B| + |A & C| + .. + |C & D|) + (|A & B & C| + |B & C & D|) - |A + B + C + D|
subset_powers_size_even = subset_powers_data.size.even?
# sum all components of equation except for the last one |A & B & C & D| = (|A| + |B| + |C| + |D|) - (|A & B| + |A & C| + .. + |C & D|) + (|A & B & C| + |B & C & D|) - ... =>
sum_of_all_subset_powers = sum_subset_powers(subset_powers_data, subset_powers_size_even)
# add last component of the equation |A & B & C & D| = sum_of_all_subset_powers - |A + B + C + D|
sum_of_all_subset_powers + (subset_powers_size_even ? power_of_union_of_all_events : -power_of_union_of_all_events)
end
def sum_subset_powers(subset_powers_data, subset_powers_size_even)
sum_without_sign = subset_powers_data.to_enum.with_index.sum do |value, index|
(index + 1).odd? ? value : -value
end
(subset_powers_size_even ? -1 : 1) * sum_without_sign
end
def subsets_intersection_powers(event_names, start_date, end_date, subset_powers_cache)
subset_sizes = (1..(event_names.size - 1))
subset_sizes.map do |subset_size|
if subset_size > 1
# calculate sum of powers of intersection between each subset (with given size) of metrics: #|A + B + C + D| = ... - (|A & B| + |A & C| + .. + |C & D|)
event_names.combination(subset_size).sum do |events_subset|
subset_powers_cache[subset_size][events_subset.join('_&_')] ||= \
calculate_events_intersections(event_names: events_subset, start_date: start_date, end_date: end_date, subset_powers_cache: subset_powers_cache)
end
else
# calculate sum of powers of each set (metric) alone #|A + B + C + D| = (|A| + |B| + |C| + |D|) - ...
event_names.sum do |event|
subset_powers_cache[subset_size][event] ||= \
unique_events(event_names: event, start_date: start_date, end_date: end_date)
end
end
end
end
def calculate_events_union(event_names:, start_date:, end_date:)
count_unique_events(event_names: event_names, start_date: start_date, end_date: end_date) do |events|
raise SlotMismatch, events unless events_in_same_slot?(events)
raise AggregationMismatch, events unless events_same_aggregation?(events)
end
end
def count_unique_events(event_names:, start_date:, end_date:, context: '')
events = events_for(Array(event_names).map(&:to_s))
@ -340,12 +251,6 @@ module Gitlab
end.flatten
end
def validate_aggregation_operator!(operator)
return true if ALLOWED_METRICS_AGGREGATIONS.include?(operator)
raise UnknownAggregationOperator.new("Events should be aggregated with one of operators #{ALLOWED_METRICS_AGGREGATIONS}")
end
def weekly_redis_keys(events:, start_date:, end_date:, context: '')
end_date = end_date.end_of_week - 1.week
(start_date.to_date..end_date.to_date).map do |date|

View file

@ -63,53 +63,20 @@ RSpec.describe Projects::IssuesController do
end
end
describe 'the null hypothesis experiment', :snowplow do
describe 'the null hypothesis experiment', :experiment do
before do
stub_experiments(null_hypothesis: :candidate)
end
it 'defines the expected before actions' do
expect(controller).to use_before_action(:run_null_hypothesis_experiment)
end
context 'when rolled out to 100%' do
it 'assigns the candidate experience and tracks the event' do
get :index, params: { namespace_id: project.namespace, project_id: project }
it 'assigns the candidate experience and tracks the event' do
expect(experiment(:null_hypothesis)).to track('index').on_any_instance.for(:candidate)
.with_context(project: project)
expect_snowplow_event(
category: 'null_hypothesis',
action: 'index',
context: [{
schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0',
data: { variant: 'candidate', experiment: 'null_hypothesis', key: anything }
}]
)
end
end
context 'when not rolled out' do
before do
stub_feature_flags(null_hypothesis: false)
end
it 'assigns the control experience and tracks the event' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect_snowplow_event(
category: 'null_hypothesis',
action: 'index',
context: [{
schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0',
data: { variant: 'control', experiment: 'null_hypothesis', key: anything }
}]
)
end
end
context 'when gitlab_experiments is disabled' do
it 'does not run the experiment at all' do
stub_feature_flags(gitlab_experiments: false)
expect(controller).not_to receive(:run_null_hypothesis_experiment)
get :index, params: { namespace_id: project.namespace, project_id: project }
end
get :index, params: { namespace_id: project.namespace, project_id: project }
end
end
end

View file

@ -258,6 +258,20 @@ RSpec.describe SearchController do
it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' }
end
describe 'GET #opensearch' do
render_views
it 'renders xml' do
get :opensearch, format: :xml
doc = Nokogiri::XML.parse(response.body)
expect(response).to have_gitlab_http_status(:ok)
expect(doc.css('OpenSearchDescription ShortName').text).to eq('GitLab')
expect(doc.css('OpenSearchDescription *').map(&:name)).to eq(%w[ShortName Description InputEncoding Image Url SearchForm])
end
end
describe '#append_info_to_payload' do
it 'appends search metadata for logging' do
last_payload = nil

View file

@ -2,15 +2,51 @@
require 'spec_helper'
RSpec.describe ApplicationExperiment do
RSpec.describe ApplicationExperiment, :experiment do
subject { described_class.new(:stub) }
before do
allow(subject).to receive(:enabled?).and_return(true)
end
it "naively assumes a 1x1 relationship to feature flags for tests" do
expect(Feature).to receive(:persist_used!).with('stub')
described_class.new(:stub)
end
describe "enabled" do
before do
allow(subject).to receive(:enabled?).and_call_original
allow(Feature::Definition).to receive(:get).and_return('_instance_')
allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
allow(Feature).to receive(:get).and_return(double(state: :on))
end
it "is enabled when all criteria are met" do
expect(subject).to be_enabled
end
it "isn't enabled if the feature definition doesn't exist" do
expect(Feature::Definition).to receive(:get).with('stub').and_return(nil)
expect(subject).not_to be_enabled
end
it "isn't enabled if we're not in dev or dotcom environments" do
expect(Gitlab).to receive(:dev_env_or_com?).and_return(false)
expect(subject).not_to be_enabled
end
it "isn't enabled if the feature flag state is :off" do
expect(Feature).to receive(:get).with('stub').and_return(double(state: :off))
expect(subject).not_to be_enabled
end
end
describe "publishing results" do
it "tracks the assignment" do
expect(subject).to receive(:track).with(:assignment)
@ -37,8 +73,8 @@ RSpec.describe ApplicationExperiment do
end
describe "tracking events", :snowplow do
it "doesn't track if excluded" do
subject.exclude { true }
it "doesn't track if we shouldn't track" do
allow(subject).to receive(:should_track?).and_return(false)
subject.track(:action)

View file

@ -170,6 +170,8 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Ignores invalid: <%= User.reference_prefix %>fake_user
- Ignored in code: `<%= user.to_reference %>`
- Ignored in links: [Link to <%= user.to_reference %>](#user-link)
- Ignored when backslash escaped: \<%= user.to_reference %>
- Ignored when backslash escaped: \<%= group.to_reference %>
- Link to user by reference: [User](<%= user.to_reference %>)
#### IssueReferenceFilter
@ -178,6 +180,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Issue in another project: <%= xissue.to_reference(project) %>
- Ignored in code: `<%= issue.to_reference %>`
- Ignored in links: [Link to <%= issue.to_reference %>](#issue-link)
- Ignored when backslash escaped: \<%= issue.to_reference %>
- Issue by URL: <%= urls.project_issue_url(issue.project, issue) %>
- Link to issue by reference: [Issue](<%= issue.to_reference %>)
- Link to issue by URL: [Issue](<%= urls.project_issue_url(issue.project, issue) %>)
@ -188,6 +191,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Merge request in another project: <%= xmerge_request.to_reference(project) %>
- Ignored in code: `<%= merge_request.to_reference %>`
- Ignored in links: [Link to <%= merge_request.to_reference %>](#merge-request-link)
- Ignored when backslash escaped: \<%= merge_request.to_reference %>
- Merge request by URL: <%= urls.project_merge_request_url(merge_request.project, merge_request) %>
- Link to merge request by reference: [Merge request](<%= merge_request.to_reference %>)
- Link to merge request by URL: [Merge request](<%= urls.project_merge_request_url(merge_request.project, merge_request) %>)
@ -198,6 +202,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Snippet in another project: <%= xsnippet.to_reference(project) %>
- Ignored in code: `<%= snippet.to_reference %>`
- Ignored in links: [Link to <%= snippet.to_reference %>](#snippet-link)
- Ignored when backslash escaped: \<%= snippet.to_reference %>
- Snippet by URL: <%= urls.project_snippet_url(snippet.project, snippet) %>
- Link to snippet by reference: [Snippet](<%= snippet.to_reference %>)
- Link to snippet by URL: [Snippet](<%= urls.project_snippet_url(snippet.project, snippet) %>)
@ -229,6 +234,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Label by name in quotes: <%= label.to_reference(format: :name) %>
- Ignored in code: `<%= simple_label.to_reference %>`
- Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link)
- Ignored when backslash escaped: \<%= simple_label.to_reference %>
- Link to label by reference: [Label](<%= label.to_reference %>)
#### MilestoneReferenceFilter
@ -239,6 +245,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Milestone in another project: <%= xmilestone.to_reference(project) %>
- Ignored in code: `<%= simple_milestone.to_reference %>`
- Ignored in links: [Link to <%= simple_milestone.to_reference %>](#milestone-link)
- Ignored when backslash escaped: \<%= simple_milestone.to_reference %>
- Milestone by URL: <%= urls.milestone_url(milestone) %>
- Link to milestone by URL: [Milestone](<%= milestone.to_reference %>)
- Group milestone by name: <%= Milestone.reference_prefix %><%= group_milestone.name %>
@ -250,6 +257,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Alert in another project: <%= xalert.to_reference(project) %>
- Ignored in code: `<%= alert.to_reference %>`
- Ignored in links: [Link to <%= alert.to_reference %>](#alert-link)
- Ignored when backslash escaped: \<%= alert.to_reference %>
- Alert by URL: <%= alert.details_url %>
- Link to alert by reference: [Alert](<%= alert.to_reference %>)
- Link to alert by URL: [Alert](<%= alert.details_url %>)

View file

@ -169,9 +169,9 @@ RSpec.describe DiffHelper do
it "returns strings with marked inline diffs" do
marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
expect(marked_old_line).to eq(%q{abc <span class="idiff left right deletion">&#39;def&#39;</span>})
expect(marked_old_line).to eq(%q{abc <span class="idiff left deletion">&#39;</span>def<span class="idiff right deletion">&#39;</span>})
expect(marked_old_line).to be_html_safe
expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">&quot;def&quot;</span>})
expect(marked_new_line).to eq(%q{abc <span class="idiff left addition">&quot;</span>def<span class="idiff right addition">&quot;</span>})
expect(marked_new_line).to be_html_safe
end

Some files were not shown because too many files have changed in this diff Show more