Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6e495b4e91
commit
9ecdb93f4e
130 changed files with 2015 additions and 1055 deletions
2
Gemfile
2
Gemfile
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -67,6 +67,9 @@ class SearchController < ApplicationController
|
|||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def opensearch
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# overridden in EE
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' }
|
||||
|
|
9
app/views/search/opensearch.xml.erb
Normal file
9
app/views/search/opensearch.xml.erb
Normal 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>
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Escaped markdown should not be interpreted as shortcuts
|
||||
merge_request: 45922
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve highlighting for merge diffs
|
||||
merge_request: 52499
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/262860-persist-shift-job.yml
Normal file
5
changelogs/unreleased/262860-persist-shift-job.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add job to persist On-call shifts
|
||||
merge_request: 50239
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/267140-check-if-user-is-bot.yml
Normal file
5
changelogs/unreleased/267140-check-if-user-is-bot.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Expose if user is a bot in the REST api
|
||||
merge_request: 52003
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/298975-search-autodiscovery.yml
Normal file
5
changelogs/unreleased/298975-search-autodiscovery.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add OpenSearch support
|
||||
merge_request: 52583
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Project creation to use specialized worker to calculate project authorizations
|
||||
merge_request: 52719
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Make LifecycleEvents exceptions to be fatal
|
||||
merge_request: 52881
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Monitor RackAttack redis usage and enrich auth structured logs
|
||||
merge_request: 52471
|
||||
author:
|
||||
type: changed
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -27,6 +27,7 @@ exceptions:
|
|||
- CNA
|
||||
- CNAME
|
||||
- CORE
|
||||
- CVS
|
||||
- FREE
|
||||
- CPU
|
||||
- CRIME
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
40
lib/banzai/filter/markdown_post_escape_filter.rb
Normal file
40
lib/banzai/filter/markdown_post_escape_filter.rb
Normal 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
|
43
lib/banzai/filter/markdown_pre_escape_filter.rb
Normal file
43
lib/banzai/filter/markdown_pre_escape_filter.rb
Normal 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
|
|
@ -5,7 +5,9 @@ module Banzai
|
|||
class PlainMarkdownPipeline < BasePipeline
|
||||
def self.filters
|
||||
FilterArray[
|
||||
Filter::MarkdownFilter
|
||||
Filter::MarkdownPreEscapeFilter,
|
||||
Filter::MarkdownFilter,
|
||||
Filter::MarkdownPostEscapeFilter
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
26
lib/bulk_imports/pipeline/extracted_data.rb
Normal file
26
lib/bulk_imports/pipeline/extracted_data.rb
Normal 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
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
||||
|
|
74
lib/gitlab/diff/char_diff.rb
Normal file
74
lib/gitlab/diff/char_diff.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
#
|
||||
|
|
91
lib/gitlab/metrics/subscribers/rack_attack.rb
Normal file
91
lib/gitlab/metrics/subscribers/rack_attack.rb
Normal 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
|
|
@ -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)
|
||||
|
||||
|
|
32
lib/gitlab/rack_attack/instrumented_cache_store.rb
Normal file
32
lib/gitlab/rack_attack/instrumented_cache_store.rb
Normal 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
|
133
lib/gitlab/usage/metrics/aggregates/aggregate.rb
Normal file
133
lib/gitlab/usage/metrics/aggregates/aggregate.rb
Normal 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
|
|
@ -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,
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
8
spec/fixtures/markdown.md.erb
vendored
8
spec/fixtures/markdown.md.erb
vendored
|
@ -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 %>)
|
||||
|
|
|
@ -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">'def'</span>})
|
||||
expect(marked_old_line).to eq(%q{abc <span class="idiff left deletion">'</span>def<span class="idiff right deletion">'</span>})
|
||||
expect(marked_old_line).to be_html_safe
|
||||
expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">"def"</span>})
|
||||
expect(marked_new_line).to eq(%q{abc <span class="idiff left addition">"</span>def<span class="idiff right addition">"</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
Loading…
Reference in a new issue