Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/dast.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/reports.gitlab-ci.yml3
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml8
-rw-r--r--.rubocop_todo.yml2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue22
-rw-r--r--app/assets/javascripts/content_editor/components/divider.vue3
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_button.vue64
-rw-r--r--app/assets/javascripts/content_editor/components/top_toolbar.vue66
-rw-r--r--app/assets/javascripts/content_editor/services/create_editor.js10
-rw-r--r--app/models/ability.rb2
-rw-r--r--app/models/user.rb6
-rw-r--r--app/policies/base_policy.rb2
-rw-r--r--app/services/system_hooks_service.rb24
-rw-r--r--app/views/admin/application_settings/_terminal.html.haml7
-rw-r--r--app/views/admin/spam_logs/index.html.haml22
-rw-r--r--app/views/devise/confirmations/new.html.haml4
-rw-r--r--app/views/devise/shared/_links.erb10
-rw-r--r--changelogs/unreleased/Externalise-strings-in-confirmations-new-html-haml.yml5
-rw-r--r--changelogs/unreleased/Externalize-strings-in-_terminal-html-haml.yml5
-rw-r--r--changelogs/unreleased/Externalize-strings-in-shared_links-erb.yml5
-rw-r--r--changelogs/unreleased/Externalizes-strings-in-spam_logs-index-html-haml.yml5
-rw-r--r--changelogs/unreleased/remove-longer-count-cache-validity-ff.yml5
-rw-r--r--changelogs/unreleased/update-auto-build-image-to-0-6-0.yml5
-rw-r--r--config/feature_flags/development/longer_count_cache_validity.yml8
-rw-r--r--config/initializers/stackprof.rb6
-rw-r--r--doc/administration/pages/index.md6
-rw-r--r--doc/api/projects.md35
-rw-r--r--doc/development/changelog.md1
-rw-r--r--doc/development/code_review.md6
-rw-r--r--doc/development/performance.md4
-rw-r--r--doc/development/policies.md2
-rw-r--r--doc/install/aws/index.md6
-rw-r--r--doc/update/mysql_to_postgresql.md2
-rw-r--r--doc/update/upgrading_postgresql_using_slony.md2
-rw-r--r--doc/user/admin_area/img/abuse_reports_page_v13_11.pngbin77994 -> 25232 bytes
-rw-r--r--doc/user/analytics/img/code_review_analytics_v13_11.pngbin107184 -> 37179 bytes
-rw-r--r--doc/user/analytics/img/issues_created_per_month_v13_11.pngbin57729 -> 21731 bytes
-rw-r--r--doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_11.pngbin45002 -> 15732 bytes
-rw-r--r--doc/user/group/insights/img/insights_example_stacked_bar_chart_v13_11.pngbin85296 -> 29784 bytes
-rw-r--r--doc/user/group/roadmap/img/roadmap_filters_v13_11.pngbin79098 -> 19966 bytes
-rw-r--r--doc/user/img/snippet_intro_v13_11.pngbin37571 -> 15293 bytes
-rw-r--r--doc/user/project/repository/repository_mirroring.md2
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml4
-rw-r--r--lib/gitlab/hook_data/project_builder.rb57
-rw-r--r--lib/gitlab/usage/metrics/aggregates/aggregate.rb63
-rw-r--r--lib/gitlab/usage/metrics/aggregates/sources/calculations/intersection.rb76
-rw-r--r--lib/gitlab/usage/metrics/aggregates/sources/postgres_hll.rb1
-rw-r--r--lib/gitlab/usage/metrics/aggregates/sources/redis_hll.rb1
-rw-r--r--locale/gitlab.pot36
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap9
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js26
-rw-r--r--spec/frontend/content_editor/components/toolbar_button_spec.js85
-rw-r--r--spec/frontend/content_editor/components/top_toolbar_spec.js46
-rw-r--r--spec/frontend/content_editor/services/create_editor_spec.js12
-rw-r--r--spec/lib/gitlab/hook_data/project_builder_spec.rb83
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb80
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/sources/calculations/intersection_spec.rb89
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb60
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb35
61 files changed, 866 insertions, 268 deletions
diff --git a/.gitlab/ci/dast.gitlab-ci.yml b/.gitlab/ci/dast.gitlab-ci.yml
index a8a201bd1fa..309714f8739 100644
--- a/.gitlab/ci/dast.gitlab-ci.yml
+++ b/.gitlab/ci/dast.gitlab-ci.yml
@@ -3,7 +3,7 @@
- prm
# For scheduling dast job
extends:
- - .reports:schedule-dast
+ - .reports:rules:schedule-dast
image:
name: "registry.gitlab.com/gitlab-org/security-products/dast:$DAST_VERSION"
resource_group: dast_scan
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml
index 4d54380cefe..2f02e6c1f97 100644
--- a/.gitlab/ci/reports.gitlab-ci.yml
+++ b/.gitlab/ci/reports.gitlab-ci.yml
@@ -143,12 +143,13 @@ dependency_scanning gemnasium-python:
# See https://gitlab.com/gitlab-com/gl-security/security-research/package-hunter
package_hunter:
extends:
- - .reports:schedule-dast
+ - .reports:rules:package_hunter
stage: test
image:
name: registry.gitlab.com/gitlab-com/gl-security/security-research/package-hunter-cli:latest
entrypoint: [""]
needs: []
+ allow_failure: true
script:
- rm -r spec locale .git app/assets/images doc/
- cd .. && tar -I "gzip --best" -cf gitlab.tgz gitlab/
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index c2d16582a68..f1c918d8c00 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -996,13 +996,19 @@
when: manual
allow_failure: true
-.reports:schedule-dast:
+.reports:rules:schedule-dast:
rules:
- if: '$DAST_DISABLED || $GITLAB_FEATURES !~ /\bdast\b/'
when: never
- <<: *if-default-branch-schedule-nightly
allow_failure: true
+.reports:rules:package_hunter:
+ rules:
+ - <<: *if-default-branch-schedule-2-hourly
+ - <<: *if-merge-request
+ changes: ["yarn.lock"]
+
.reports:rules:license_scanning:
rules:
- if: '$LICENSE_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\blicense_scanning\b/'
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 4bd885c35d2..765c39285e8 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -354,8 +354,6 @@ Performance/Sum:
- 'ee/spec/lib/gitlab/elastic/bulk_indexer_spec.rb'
- 'lib/api/entities/issuable_time_stats.rb'
- 'lib/container_registry/tag.rb'
- - 'lib/declarative_policy/rule.rb'
- - 'lib/declarative_policy/runner.rb'
- 'lib/gitlab/ci/reports/test_suite_comparer.rb'
- 'lib/gitlab/diff/file.rb'
- 'lib/gitlab/sherlock/transaction.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 1ad98df6dbb..5e04576a889 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-7e05394f21ee383c94854f5b6f1316d0ec94d788
+d8154c73a22b2d1a08e4a348f9463aeb5144f74c
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 839d4de912d..13ddd327c7c 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -1,18 +1,26 @@
<script>
-import { EditorContent } from 'tiptap';
-import createEditor from '../services/create_editor';
+import { EditorContent, Editor } from 'tiptap';
+import TopToolbar from './top_toolbar.vue';
export default {
components: {
EditorContent,
+ TopToolbar,
},
- data() {
- return {
- editor: createEditor(),
- };
+ props: {
+ editor: {
+ type: Object,
+ required: true,
+ validator: (editor) => editor instanceof Editor,
+ },
},
};
</script>
<template>
- <editor-content :editor="editor" />
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-p-3 gl-border-solid gl-border-1 gl-border-gray-200 gl-rounded-base"
+ >
+ <top-toolbar class="gl-mb-3" :editor="editor" />
+ <editor-content class="md" :editor="editor" />
+ </div>
</template>
diff --git a/app/assets/javascripts/content_editor/components/divider.vue b/app/assets/javascripts/content_editor/components/divider.vue
new file mode 100644
index 00000000000..b77bd7b7cf3
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/divider.vue
@@ -0,0 +1,3 @@
+<template>
+ <span class="gl-mx-3 gl-border-r-solid gl-border-r-1 gl-border-gray-200"></span>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_button.vue b/app/assets/javascripts/content_editor/components/toolbar_button.vue
new file mode 100644
index 00000000000..be3cd02fccc
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/toolbar_button.vue
@@ -0,0 +1,64 @@
+<script>
+import { GlButton, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ },
+ directives: {
+ GlTooltip,
+ },
+ props: {
+ iconName: {
+ type: String,
+ required: true,
+ },
+ editor: {
+ type: Object,
+ required: true,
+ },
+ contentType: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ executeCommand: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ computed: {
+ isActive() {
+ return this.editor.isActive[this.contentType]() && this.editor.focused;
+ },
+ },
+ methods: {
+ execute() {
+ const { contentType } = this;
+
+ if (this.executeCommand) {
+ this.editor.commands[contentType]();
+ }
+
+ this.$emit('click', { contentType });
+ },
+ },
+};
+</script>
+<template>
+ <gl-button
+ v-gl-tooltip
+ category="tertiary"
+ size="small"
+ class="gl-mx-2"
+ :class="{ active: isActive }"
+ :aria-label="label"
+ :title="label"
+ :icon="iconName"
+ @click="execute"
+ />
+</template>
diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue
new file mode 100644
index 00000000000..9fdbb7c34fb
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue
@@ -0,0 +1,66 @@
+<script>
+import Divider from './divider.vue';
+import ToolbarButton from './toolbar_button.vue';
+
+export default {
+ components: {
+ ToolbarButton,
+ Divider,
+ },
+ props: {
+ editor: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div
+ class="gl-display-flex gl-justify-content-end gl-pb-3 gl-pt-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200"
+ >
+ <toolbar-button
+ data-testid="bold"
+ content-type="bold"
+ icon-name="bold"
+ :label="__('Bold text')"
+ :editor="editor"
+ />
+ <toolbar-button
+ data-testid="italic"
+ content-type="italic"
+ icon-name="italic"
+ :label="__('Italic text')"
+ :editor="editor"
+ />
+ <toolbar-button
+ data-testid="code"
+ content-type="code"
+ icon-name="code"
+ :label="__('Code')"
+ :editor="editor"
+ />
+ <divider />
+ <toolbar-button
+ data-testid="blockquote"
+ content-type="blockquote"
+ icon-name="quote"
+ :label="__('Insert a quote')"
+ :editor="editor"
+ />
+ <toolbar-button
+ data-testid="bullet-list"
+ content-type="bullet_list"
+ icon-name="list-bulleted"
+ :label="__('Add a bullet list')"
+ :editor="editor"
+ />
+ <toolbar-button
+ data-testid="ordered-list"
+ content-type="ordered_list"
+ icon-name="list-numbered"
+ :label="__('Add a numbered list')"
+ :editor="editor"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/content_editor/services/create_editor.js b/app/assets/javascripts/content_editor/services/create_editor.js
index 128d332b0a2..f46129db389 100644
--- a/app/assets/javascripts/content_editor/services/create_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_editor.js
@@ -37,6 +37,16 @@ const createEditor = async ({ content, renderMarkdown, serializer: customSeriali
new OrderedList(),
new CodeBlockHighlight(),
],
+ editorProps: {
+ attributes: {
+ /*
+ * Adds some padding to the contenteditable element where the user types.
+ * Otherwise, the text cursor is not visible when its position is at the
+ * beginning of a line.
+ */
+ class: 'gl-py-4 gl-px-5',
+ },
+ },
});
const serializer = customSerializer || createMarkdownSerializer({ render: renderMarkdown });
diff --git a/app/models/ability.rb b/app/models/ability.rb
index ba46a98b951..c18bd21d754 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require_dependency 'declarative_policy'
-
class Ability
class << self
# Given a list of users and a project this method returns the users that can
diff --git a/app/models/user.rb b/app/models/user.rb
index 507e8cc2cf5..4f9874ab94b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1620,11 +1620,7 @@ class User < ApplicationRecord
end
def count_cache_validity_period
- if Feature.enabled?(:longer_count_cache_validity, self, default_enabled: :yaml)
- 24.hours
- else
- 20.minutes
- end
+ 24.hours
end
def assigned_open_merge_requests_count(force: false)
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 1c19751cf0d..4644adad469 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require_dependency 'declarative_policy'
-
class BasePolicy < DeclarativePolicy::Base
desc "User is an instance admin"
with_options scope: :user, score: 0
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 53e810035c5..d1742841910 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class SystemHooksService
- BUILDER_DRIVEN_EVENT_DATA_AVAILABLE_FOR_CLASSES = [GroupMember, Group, ProjectMember, User].freeze
+ BUILDER_DRIVEN_EVENT_DATA_AVAILABLE_FOR_CLASSES = [GroupMember, Group, ProjectMember, User, Project].freeze
def execute_hooks_for(model, event)
data = build_event_data(model, event)
@@ -41,12 +41,6 @@ class SystemHooksService
if model.user
data[:username] = model.user.username
end
- when Project
- data.merge!(project_data(model))
-
- if event == :rename || event == :transfer
- data[:old_path_with_namespace] = model.old_path_with_namespace
- end
end
data
@@ -56,20 +50,6 @@ class SystemHooksService
"#{model.class.name.downcase}_#{event}"
end
- def project_data(model)
- owner = model.owner
-
- {
- name: model.name,
- path: model.path,
- path_with_namespace: model.full_path,
- project_id: model.id,
- owner_name: owner.name,
- owner_email: owner.respond_to?(:email) ? owner.email : "",
- project_visibility: model.visibility.downcase
- }
- end
-
def builder_driven_event_data_available?(model)
model.class.in?(BUILDER_DRIVEN_EVENT_DATA_AVAILABLE_FOR_CLASSES)
end
@@ -84,6 +64,8 @@ class SystemHooksService
Gitlab::HookData::ProjectMemberBuilder
when User
Gitlab::HookData::UserBuilder
+ when Project
+ Gitlab::HookData::ProjectBuilder
end
builder_class.new(model).build(event)
diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml
index 482466c4b3b..d6e31a24cf6 100644
--- a/app/views/admin/application_settings/_terminal.html.haml
+++ b/app/views/admin/application_settings/_terminal.html.haml
@@ -3,9 +3,8 @@
%fieldset
.form-group
- = f.label :terminal_max_session_time, 'Max session time', class: 'label-bold'
+ = f.label :terminal_max_session_time, _('Max session time'), class: 'label-bold'
= f.number_field :terminal_max_session_time, class: 'form-control gl-form-input'
.form-text.text-muted
- Maximum time for web terminal websocket connection (in seconds).
- 0 for unlimited.
- = f.submit 'Save changes', class: "gl-button btn btn-confirm"
+ = _('Maximum time for web terminal websocket connection (in seconds). 0 for unlimited.')
+ = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml
index 40fbc559d72..2a36c991ed2 100644
--- a/app/views/admin/spam_logs/index.html.haml
+++ b/app/views/admin/spam_logs/index.html.haml
@@ -1,22 +1,22 @@
- page_title _("Spam Logs")
-%h3.page-title Spam Logs
+%h3.page-title= _('Spam Logs')
%hr
- if @spam_logs.present?
.table-holder
%table.table
%thead
%tr
- %th Date
- %th User
- %th Source IP
- %th API?
- %th Recaptcha verified?
- %th Type
- %th Title
- %th Description
- %th Primary Action
+ %th= _('Date')
+ %th= _('User')
+ %th= _('Source IP')
+ %th= _('API?')
+ %th= _('Recaptcha verified?')
+ %th= _('Type')
+ %th= _('Title')
+ %th= _('Description')
+ %th= _('Primary Action')
%th
= render @spam_logs
= paginate @spam_logs, theme: 'gitlab'
- else
- %h4 There are no Spam Logs
+ %h4= _('There are no Spam Logs')
diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml
index 024ccaddaa1..51354618aa4 100644
--- a/app/views/devise/confirmations/new.html.haml
+++ b/app/views/devise/confirmations/new.html.haml
@@ -6,9 +6,9 @@
= render "devise/shared/error_messages", resource: resource
.form-group
= f.label :email
- = f.email_field :email, class: "form-control gl-form-input", required: true, title: 'Please provide a valid email address.', value: nil
+ = f.email_field :email, class: "form-control gl-form-input", required: true, title: _('Please provide a valid email address.'), value: nil
.clearfix
- = f.submit "Resend", class: 'gl-button btn btn-confirm'
+ = f.submit _("Resend"), class: 'gl-button btn btn-confirm'
.clearfix.prepend-top-20
= render 'devise/shared/sign_in_link'
diff --git a/app/views/devise/shared/_links.erb b/app/views/devise/shared/_links.erb
index cb934434c28..f0215f5ea42 100644
--- a/app/views/devise/shared/_links.erb
+++ b/app/views/devise/shared/_links.erb
@@ -1,19 +1,19 @@
<%- if controller_name != 'sessions' %>
- <%= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: "btn" %><br />
+ <%= link_to _("Sign in"), new_session_path(:user, redirect_to_referer: 'yes'), class: "btn" %><br />
<% end -%>
<%- if devise_mapping.registerable? && controller_name != 'registrations' && allow_signup? %>
- <%= link_to "Sign up", new_registration_path(:user) %><br />
+ <%= link_to _("Sign up"), new_registration_path(:user) %><br />
<% end -%>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' %>
-<%= link_to "Forgot your password?", new_password_path(:user), class: "btn" %><br />
+<%= link_to _("Forgot your password?"), new_password_path(:user), class: "btn" %><br />
<% end -%>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
- <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(:user) %><br />
+ <%= link_to _("Didn't receive confirmation instructions?"), new_confirmation_path(:user) %><br />
<% end -%>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
- <%= link_to "Didn't receive unlock instructions?", new_unlock_path(:user) %><br />
+ <%= link_to _("Didn't receive unlock instructions?"), new_unlock_path(:user) %><br />
<% end -%>
diff --git a/changelogs/unreleased/Externalise-strings-in-confirmations-new-html-haml.yml b/changelogs/unreleased/Externalise-strings-in-confirmations-new-html-haml.yml
new file mode 100644
index 00000000000..05285025d93
--- /dev/null
+++ b/changelogs/unreleased/Externalise-strings-in-confirmations-new-html-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Externalise strings in confirmations/new.html.haml
+merge_request: 58173
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Externalize-strings-in-_terminal-html-haml.yml b/changelogs/unreleased/Externalize-strings-in-_terminal-html-haml.yml
new file mode 100644
index 00000000000..b7fc8abe3f6
--- /dev/null
+++ b/changelogs/unreleased/Externalize-strings-in-_terminal-html-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings in application_settings/_terminal.html.haml
+merge_request: 58081
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Externalize-strings-in-shared_links-erb.yml b/changelogs/unreleased/Externalize-strings-in-shared_links-erb.yml
new file mode 100644
index 00000000000..a0d47811cce
--- /dev/null
+++ b/changelogs/unreleased/Externalize-strings-in-shared_links-erb.yml
@@ -0,0 +1,5 @@
+---
+title: Externalise strings in shared/_links.erb
+merge_request: 58278
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Externalizes-strings-in-spam_logs-index-html-haml.yml b/changelogs/unreleased/Externalizes-strings-in-spam_logs-index-html-haml.yml
new file mode 100644
index 00000000000..ce310687e11
--- /dev/null
+++ b/changelogs/unreleased/Externalizes-strings-in-spam_logs-index-html-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Externalises strings in spam_logs/index.html.haml
+merge_request: 58170
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/remove-longer-count-cache-validity-ff.yml b/changelogs/unreleased/remove-longer-count-cache-validity-ff.yml
new file mode 100644
index 00000000000..fefa9ce1f8d
--- /dev/null
+++ b/changelogs/unreleased/remove-longer-count-cache-validity-ff.yml
@@ -0,0 +1,5 @@
+---
+title: Remove the longer_count_cache_validity_period feature flag
+merge_request: 59746
+author:
+type: performance
diff --git a/changelogs/unreleased/update-auto-build-image-to-0-6-0.yml b/changelogs/unreleased/update-auto-build-image-to-0-6-0.yml
deleted file mode 100644
index 8b17a649651..00000000000
--- a/changelogs/unreleased/update-auto-build-image-to-0-6-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update auto-build-image to v0.6.0, updating the included docker to 20.10.6 and pack to 0.18.0
-merge_request: 59525
-author:
-type: changed
diff --git a/config/feature_flags/development/longer_count_cache_validity.yml b/config/feature_flags/development/longer_count_cache_validity.yml
deleted file mode 100644
index 380eaafac44..00000000000
--- a/config/feature_flags/development/longer_count_cache_validity.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: longer_count_cache_validity
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57122
-rollout_issue_url:
-milestone: '13.11'
-type: development
-group: group::source code
-default_enabled: false
diff --git a/config/initializers/stackprof.rb b/config/initializers/stackprof.rb
index 4c4d241f065..2420821c4b2 100644
--- a/config/initializers/stackprof.rb
+++ b/config/initializers/stackprof.rb
@@ -9,10 +9,10 @@ module Gitlab
DEFAULT_FILE_PREFIX = Dir.tmpdir
DEFAULT_TIMEOUT_SEC = 30
DEFAULT_MODE = :cpu
- # Sample interval as a frequency in microseconds (~100hz); appropriate for CPU profiles
- DEFAULT_INTERVAL_US = 10_000
+ # Sample interval as a frequency in microseconds (~99hz); appropriate for CPU profiles
+ DEFAULT_INTERVAL_US = 10_100
# Sample interval in event occurrences (n = every nth event); appropriate for allocation profiles
- DEFAULT_INTERVAL_EVENTS = 1_000
+ DEFAULT_INTERVAL_EVENTS = 100
# this is a workaround for sidekiq, which defines its own SIGUSR2 handler.
# by defering to the sidekiq startup event, we get to set up our own
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index d04688dab7a..ae4fa086e3f 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -1001,7 +1001,7 @@ to using that.
### Migrate Pages deployments to object storage
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/325285) in GitLab 13.11
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/325285) in GitLab 13.11.
Existing Pages deployments objects (which store [ZIP archives](#zip-storage)) can similarly be
migrated to [object storage](#using-object-storage), if
@@ -1010,7 +1010,7 @@ you've been having them stored locally.
Migrate your existing Pages deployments from local storage to object storage:
```shell
-sudo gitlab-rails gitlab:pages:deployments:migrate_to_object_storage
+sudo gitlab-rake gitlab:pages:deployments:migrate_to_object_storage
```
### Rolling Pages deployments back to local storage
@@ -1018,7 +1018,7 @@ sudo gitlab-rails gitlab:pages:deployments:migrate_to_object_storage
After the migration to object storage is performed, you can choose to revert your Pages deployments back to local storage:
```shell
-sudo gitlab-rails gitlab:pages:deployments:migrate_to_local
+sudo gitlab-rake gitlab:pages:deployments:migrate_to_local
```
## Backup
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 50c1356dfd8..d9aabfbc337 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -2016,6 +2016,41 @@ The returned `url` is relative to the project path. The returned `full_path` is
the absolute path to the file. In Markdown contexts, the link is expanded when
the format in `markdown` is used.
+### Max attachment size enforcement
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57250) in GitLab 13.11.
+
+GitLab 13.11 added enforcement of the [maximum attachment size limit](../user/admin_area/settings/account_and_limit_settings.md#max-attachment-size) behind the `enforce_max_attachment_size_upload_api` feature flag. GitLab 14.0 will enable this by default.
+
+**In Omnibus installations:**
+
+1. Enter the Rails console:
+
+ ```shell
+ sudo gitlab-rails console
+ ```
+
+1. Enable the feature flag:
+
+ ```ruby
+ Feature.enable(:enforce_max_attachment_size_upload_api)
+ ```
+
+**In installations from source:**
+
+1. Enter the Rails console:
+
+ ```shell
+ cd /home/git/gitlab
+ sudo -u git -H bundle exec rails console -e production
+ ```
+
+1. Enable the feature flag to disable the validation:
+
+ ```ruby
+ Feature.enable(:enforce_max_attachment_size_upload_api)
+ ```
+
## Upload a project avatar
Uploads an avatar to the specified project.
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index ee80d998c14..eef18d6850d 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -48,7 +48,6 @@ the `author` field. GitLab team members **should not**.
- Any client-facing change to our REST and GraphQL APIs **must** have a changelog entry. See the [complete list what comprises a GraphQL breaking change](api_graphql_styleguide.md#breaking-changes).
- Any change that introduces an [Advanced Search migration](elasticsearch.md#creating-a-new-advanced-search-migration) **must** have a changelog entry.
- Performance improvements **should** have a changelog entry.
- also require a changelog entry.
- _Any_ contribution from a community member, no matter how small, **may** have
a changelog entry regardless of these guidelines if the contributor wants one.
Example: "Fixed a typo on the search results page."
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 731fec98933..e42180c2fc2 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -299,8 +299,10 @@ first time.
of your shiny new branch, read through the entire diff. Does it make sense?
Did you include something unrelated to the overall purpose of the changes? Did
you forget to remove any debugging code?
-- Consider providing instructions on how to test the merge request. This can be
- helpful for reviewers not familiar with the product feature or area of the codebase.
+- Write a detailed description as outlined in the [merge request guidelines](contributing/merge_request_workflow.md#merge-request-guidelines).
+ Some reviewers may not be familiar with the product feature or area of the
+ codebase. Thorough descriptions help all reviewers understand your request
+ and test effectively.
- If you know your change depends on another being merged first, note it in the
description and set an [merge request dependency](../user/project/merge_requests/merge_request_dependencies.md).
- Be grateful for the reviewer's suggestions. (`Good call. I'll make that change.`)
diff --git a/doc/development/performance.md b/doc/development/performance.md
index e93dc26e4fc..889b803b934 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -257,8 +257,8 @@ The following configuration options can be configured:
Defaults to `cpu`.
- `STACKPROF_INTERVAL`: Sampling interval. Unit semantics depend on `STACKPROF_MODE`.
For `object` mode this is a per-event interval (every `nth` event is sampled)
- and defaults to `1000`.
- For other modes such as `cpu` this is a frequency and defaults to `10000` μs (100hz).
+ and defaults to `100`.
+ For other modes such as `cpu` this is a frequency interval and defaults to `10100` μs (99hz).
- `STACKPROF_FILE_PREFIX`: File path prefix where profiles are stored. Defaults
to `$TMPDIR` (often corresponds to `/tmp`).
- `STACKPROF_TIMEOUT_S`: Profiling timeout in seconds. Profiling will
diff --git a/doc/development/policies.md b/doc/development/policies.md
index c1a87990bc9..315878e19d9 100644
--- a/doc/development/policies.md
+++ b/doc/development/policies.md
@@ -68,7 +68,7 @@ Within the rule DSL, you can use:
- `can?(:other_ability)` delegates to the rules that apply to `:other_ability`. Note that this is distinct from the instance method `can?`, which can check dynamically - this only configures a delegation to another ability.
`~`, `&` and `|` operators are overridden methods in
-[`DeclarativePolicy::Rule::Base`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/declarative_policy/rule.rb).
+[`DeclarativePolicy::Rule::Base`](https://gitlab.com/gitlab-org/declarative-policy/-/blob/main/lib/declarative_policy/rule.rb).
Do not use boolean operators such as `&&` and `||` within the rule DSL,
as conditions within rule blocks are objects, not booleans. The same
diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md
index 7fde3915bf5..70ec349399a 100644
--- a/doc/install/aws/index.md
+++ b/doc/install/aws/index.md
@@ -838,3 +838,9 @@ You may have to set a password on the `root` user to prevent automatic redirects
### "The change you requested was rejected (422)"
If you see this page when trying to set a password via the web interface, make sure `external_url` in `gitlab.rb` matches the domain you are making a request from, and run `sudo gitlab-ctl reconfigure` after making any changes to it.
+
+### Some job logs are not uploaded to object storage
+
+When the GitLab deployment is scaled up to more than one node, some job logs may not be uploaded to [object storage](../../administration/object_storage.md) properly. [Incremental logging is required](../../administration/object_storage.md#incremental-logging-is-required-for-ci-to-use-object-storage) for CI to use object storage.
+
+Enable [incremental logging](../../administration/job_logs.md#enabling-incremental-logging) if it has not already been enabled.
diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md
index 9a367d218f0..cbe2381e8db 100644
--- a/doc/update/mysql_to_postgresql.md
+++ b/doc/update/mysql_to_postgresql.md
@@ -4,7 +4,7 @@ group: Database
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Migrating from MySQL to PostgreSQL
+# Migrating from MySQL to PostgreSQL **(FREE SELF)**
This guide documents how to take a working GitLab instance that uses MySQL and
migrate it to a PostgreSQL database.
diff --git a/doc/update/upgrading_postgresql_using_slony.md b/doc/update/upgrading_postgresql_using_slony.md
index c9f8f83749c..2cddaa5da8b 100644
--- a/doc/update/upgrading_postgresql_using_slony.md
+++ b/doc/update/upgrading_postgresql_using_slony.md
@@ -4,7 +4,7 @@ group: Database
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Upgrading PostgreSQL Using Slony
+# Upgrading PostgreSQL Using Slony **(FREE SELF)**
This guide describes the steps one can take to upgrade their PostgreSQL database
to the latest version without the need for hours of downtime. This guide assumes
diff --git a/doc/user/admin_area/img/abuse_reports_page_v13_11.png b/doc/user/admin_area/img/abuse_reports_page_v13_11.png
index bcb2aec9e64..ef57f45ab77 100644
--- a/doc/user/admin_area/img/abuse_reports_page_v13_11.png
+++ b/doc/user/admin_area/img/abuse_reports_page_v13_11.png
Binary files differ
diff --git a/doc/user/analytics/img/code_review_analytics_v13_11.png b/doc/user/analytics/img/code_review_analytics_v13_11.png
index e337afa3ace..b559b934a89 100644
--- a/doc/user/analytics/img/code_review_analytics_v13_11.png
+++ b/doc/user/analytics/img/code_review_analytics_v13_11.png
Binary files differ
diff --git a/doc/user/analytics/img/issues_created_per_month_v13_11.png b/doc/user/analytics/img/issues_created_per_month_v13_11.png
index 01ebde5a54d..da3340bfdc2 100644
--- a/doc/user/analytics/img/issues_created_per_month_v13_11.png
+++ b/doc/user/analytics/img/issues_created_per_month_v13_11.png
Binary files differ
diff --git a/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_11.png b/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_11.png
index 95e176b71b8..73a5c92670a 100644
--- a/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_11.png
+++ b/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_11.png
Binary files differ
diff --git a/doc/user/group/insights/img/insights_example_stacked_bar_chart_v13_11.png b/doc/user/group/insights/img/insights_example_stacked_bar_chart_v13_11.png
index 1ef49191a13..ff18a3e86a5 100644
--- a/doc/user/group/insights/img/insights_example_stacked_bar_chart_v13_11.png
+++ b/doc/user/group/insights/img/insights_example_stacked_bar_chart_v13_11.png
Binary files differ
diff --git a/doc/user/group/roadmap/img/roadmap_filters_v13_11.png b/doc/user/group/roadmap/img/roadmap_filters_v13_11.png
index d2a76b4edce..e45c7b2b5dd 100644
--- a/doc/user/group/roadmap/img/roadmap_filters_v13_11.png
+++ b/doc/user/group/roadmap/img/roadmap_filters_v13_11.png
Binary files differ
diff --git a/doc/user/img/snippet_intro_v13_11.png b/doc/user/img/snippet_intro_v13_11.png
index aa2ad5fd43a..4b6818341b7 100644
--- a/doc/user/img/snippet_intro_v13_11.png
+++ b/doc/user/img/snippet_intro_v13_11.png
Binary files differ
diff --git a/doc/user/project/repository/repository_mirroring.md b/doc/user/project/repository/repository_mirroring.md
index 980c5417da6..f6857623bc4 100644
--- a/doc/user/project/repository/repository_mirroring.md
+++ b/doc/user/project/repository/repository_mirroring.md
@@ -22,7 +22,7 @@ There are two kinds of repository mirroring supported by GitLab:
When the mirror repository is updated, all new branches, tags, and commits are visible in the
project's activity feed.
-Users with at least [Developer access](../../permissions.md) to the project can also force an
+Users with [Maintainer access](../../permissions.md) to the project can also force an
immediate update, unless:
- The mirror is already being updated.
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 92f6970e6fc..491afe3b9a3 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require_dependency 'declarative_policy'
-
module API
class Projects < ::API::Base
include PaginationParams
diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
index 196d42f3e3a..1c25d9d583b 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
@@ -1,10 +1,10 @@
build:
stage: build
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image:v0.6.0"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image:v0.4.0"
variables:
DOCKER_TLS_CERTDIR: ""
services:
- - docker:20.10.6-dind
+ - docker:19.03.12-dind
script:
- |
if [[ -z "$CI_COMMIT_TAG" ]]; then
diff --git a/lib/gitlab/hook_data/project_builder.rb b/lib/gitlab/hook_data/project_builder.rb
new file mode 100644
index 00000000000..65c237f743f
--- /dev/null
+++ b/lib/gitlab/hook_data/project_builder.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module HookData
+ class ProjectBuilder < BaseBuilder
+ alias_method :project, :object
+
+ # Sample data
+ # {
+ # event_name: "project_rename",
+ # created_at: "2021-04-19T07:05:36Z",
+ # updated_at: "2021-04-19T07:05:36Z",
+ # name: "my_project",
+ # path: "my_project",
+ # path_with_namespace: "namespace2/my_project",
+ # project_id: 1,
+ # owner_name: "John",
+ # owner_email: "user1@example.org",
+ # project_visibility: "internal",
+ # old_path_with_namespace: "old-path-with-namespace"
+ # }
+
+ def build(event)
+ [
+ event_data(event),
+ timestamps_data,
+ project_data,
+ event_specific_project_data(event)
+ ].reduce(:merge)
+ end
+
+ private
+
+ def project_data
+ owner = project.owner
+
+ {
+ name: project.name,
+ path: project.path,
+ path_with_namespace: project.full_path,
+ project_id: project.id,
+ owner_name: owner.name,
+ owner_email: owner.respond_to?(:email) ? owner.email : "",
+ project_visibility: project.visibility.downcase
+ }
+ end
+
+ def event_specific_project_data(event)
+ return {} unless event == :rename || event == :transfer
+
+ {
+ old_path_with_namespace: project.old_path_with_namespace
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/aggregates/aggregate.rb b/lib/gitlab/usage/metrics/aggregates/aggregate.rb
index f77c8cab39c..ec114042037 100644
--- a/lib/gitlab/usage/metrics/aggregates/aggregate.rb
+++ b/lib/gitlab/usage/metrics/aggregates/aggregate.rb
@@ -83,7 +83,7 @@ module Gitlab
when UNION_OF_AGGREGATED_METRICS
source.calculate_metrics_union(metric_names: aggregation[:events], start_date: start_date, end_date: end_date, recorded_at: recorded_at)
when INTERSECTION_OF_AGGREGATED_METRICS
- calculate_metrics_intersections(source: source, metric_names: aggregation[:events], start_date: start_date, end_date: end_date)
+ source.calculate_metrics_intersections(metric_names: aggregation[:events], start_date: start_date, end_date: end_date, recorded_at: recorded_at)
else
Gitlab::ErrorTracking
.track_and_raise_for_dev_exception(UnknownAggregationOperator.new("Events should be aggregated with one of operators #{ALLOWED_METRICS_AGGREGATIONS}"))
@@ -94,67 +94,6 @@ module Gitlab
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_metrics_intersections(source:, metric_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(source, metric_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_metrics = begin
- subset_powers_cache[metric_names.size][metric_names.join('_+_')] ||= \
- source.calculate_metrics_union(metric_names: metric_names, start_date: start_date, end_date: end_date, recorded_at: recorded_at)
- 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_metrics : -power_of_union_of_all_metrics)
- 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(source, metric_names, start_date, end_date, subset_powers_cache)
- subset_sizes = (1...metric_names.size)
-
- 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|)
- metric_names.combination(subset_size).sum do |metrics_subset|
- subset_powers_cache[subset_size][metrics_subset.join('_&_')] ||=
- calculate_metrics_intersections(source: source, metric_names: metrics_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|) - ...
- metric_names.sum do |metric|
- subset_powers_cache[subset_size][metric] ||= \
- source.calculate_metrics_union(metric_names: metric, start_date: start_date, end_date: end_date, recorded_at: recorded_at)
- end
- end
- end
- end
-
def load_metrics(wildcard)
Dir[wildcard].each_with_object([]) do |path, metrics|
metrics.push(*load_yaml_from_path(path))
diff --git a/lib/gitlab/usage/metrics/aggregates/sources/calculations/intersection.rb b/lib/gitlab/usage/metrics/aggregates/sources/calculations/intersection.rb
new file mode 100644
index 00000000000..dabf757c8a7
--- /dev/null
+++ b/lib/gitlab/usage/metrics/aggregates/sources/calculations/intersection.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Aggregates
+ module Sources
+ module Calculations
+ module Intersection
+ def calculate_metrics_intersections(metric_names:, start_date:, end_date:, recorded_at:, 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(metric_names, start_date, end_date, recorded_at, subset_powers_cache)
+
+ # calculate last component of the equation |A & B & C & D| = .... - |A + B + C + D|
+ power_of_union_of_all_metrics = begin
+ subset_powers_cache[metric_names.size][metric_names.join('_+_')] ||= \
+ calculate_metrics_union(metric_names: metric_names, start_date: start_date, end_date: end_date, recorded_at: recorded_at)
+ 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_metrics : -power_of_union_of_all_metrics)
+ end
+
+ private
+
+ def subsets_intersection_powers(metric_names, start_date, end_date, recorded_at, subset_powers_cache)
+ subset_sizes = (1...metric_names.size)
+
+ 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|)
+ metric_names.combination(subset_size).sum do |metrics_subset|
+ subset_powers_cache[subset_size][metrics_subset.join('_&_')] ||=
+ calculate_metrics_intersections(metric_names: metrics_subset, start_date: start_date, end_date: end_date, recorded_at: recorded_at, 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|) - ...
+ metric_names.sum do |metric|
+ subset_powers_cache[subset_size][metric] ||= \
+ calculate_metrics_union(metric_names: metric, start_date: start_date, end_date: end_date, recorded_at: recorded_at)
+ end
+ end
+ end
+ 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
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll.rb b/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll.rb
index a01efbdb1a6..3069afab147 100644
--- a/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll.rb
+++ b/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll.rb
@@ -6,6 +6,7 @@ module Gitlab
module Aggregates
module Sources
class PostgresHll
+ extend Calculations::Intersection
class << self
def calculate_metrics_union(metric_names:, start_date:, end_date:, recorded_at:)
time_period = start_date && end_date ? (start_date..end_date) : nil
diff --git a/lib/gitlab/usage/metrics/aggregates/sources/redis_hll.rb b/lib/gitlab/usage/metrics/aggregates/sources/redis_hll.rb
index f3a4dcf1e31..009b8e62543 100644
--- a/lib/gitlab/usage/metrics/aggregates/sources/redis_hll.rb
+++ b/lib/gitlab/usage/metrics/aggregates/sources/redis_hll.rb
@@ -8,6 +8,7 @@ module Gitlab
UnionNotAvailable = Class.new(AggregatedMetricError)
class RedisHll
+ extend Calculations::Intersection
def self.calculate_metrics_union(metric_names:, start_date:, end_date:, recorded_at: nil)
union = Gitlab::UsageDataCounters::HLLRedisCounter
.calculate_events_union(event_names: metric_names, start_date: start_date, end_date: end_date)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 7d5a8538317..d025807473e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1496,6 +1496,9 @@ msgstr ""
msgid "API version"
msgstr ""
+msgid "API?"
+msgstr ""
+
msgid "APIFuzzing|$VariableWithPassword"
msgstr ""
@@ -11244,6 +11247,12 @@ msgstr ""
msgid "Didn't receive a confirmation email?"
msgstr ""
+msgid "Didn't receive confirmation instructions?"
+msgstr ""
+
+msgid "Didn't receive unlock instructions?"
+msgstr ""
+
msgid "Diff content limits"
msgstr ""
@@ -19532,6 +19541,9 @@ msgstr ""
msgid "Max role"
msgstr ""
+msgid "Max session time"
+msgstr ""
+
msgid "Max unauthenticated requests per period per IP"
msgstr ""
@@ -19646,6 +19658,9 @@ msgstr ""
msgid "Maximum time between updates that a mirror can have when scheduled to synchronize."
msgstr ""
+msgid "Maximum time for web terminal websocket connection (in seconds). 0 for unlimited."
+msgstr ""
+
msgid "May"
msgstr ""
@@ -24023,6 +24038,9 @@ msgstr ""
msgid "Primary"
msgstr ""
+msgid "Primary Action"
+msgstr ""
+
msgid "Print codes"
msgstr ""
@@ -26048,6 +26066,9 @@ msgstr ""
msgid "Rebase source branch on the target branch."
msgstr ""
+msgid "Recaptcha verified?"
+msgstr ""
+
msgid "Receive alerts from manually configured Prometheus servers."
msgstr ""
@@ -26976,6 +26997,9 @@ msgstr[1] ""
msgid "Requires values to meet regular expression requirements."
msgstr ""
+msgid "Resend"
+msgstr ""
+
msgid "Resend Request"
msgstr ""
@@ -29712,6 +29736,9 @@ msgstr ""
msgid "Source Branch"
msgstr ""
+msgid "Source IP"
+msgstr ""
+
msgid "Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}"
msgstr ""
@@ -30501,6 +30528,12 @@ msgstr ""
msgid "SuperSonics|Sync Subscription details"
msgstr ""
+msgid "SuperSonics|The subscription details synced successfully."
+msgstr ""
+
+msgid "SuperSonics|There is a connectivity issue. You can no longer sync your subscription details with GitLab. Get help for the most common connectivity issues by %{connectivityHelpLinkStart}troubleshooting the activation code%{connectivityHelpLinkEnd}."
+msgstr ""
+
msgid "SuperSonics|Type"
msgstr ""
@@ -31554,6 +31587,9 @@ msgstr ""
msgid "There are no SSH keys with access to your account."
msgstr ""
+msgid "There are no Spam Logs"
+msgstr ""
+
msgid "There are no abuse reports!"
msgstr ""
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
new file mode 100644
index 00000000000..9a89e3430b4
--- /dev/null
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`content_editor/components/toolbar_button displays tertiary, small button with a provided label and icon 1`] = `
+"<b-button-stub event=\\"click\\" routertag=\\"a\\" size=\\"sm\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-mx-2 gl-button btn-default-tertiary btn-icon\\">
+ <!---->
+ <gl-icon-stub name=\\"bold\\" size=\\"16\\" class=\\"gl-button-icon\\"></gl-icon-stub>
+ <!---->
+</b-button-stub>"
+`;
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index f055a49135b..c3c8654ce3d 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -1,26 +1,36 @@
import { shallowMount } from '@vue/test-utils';
import { EditorContent } from 'tiptap';
import ContentEditor from '~/content_editor/components/content_editor.vue';
+import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import createEditor from '~/content_editor/services/create_editor';
-
-jest.mock('~/content_editor/services/create_editor');
+import createMarkdownSerializer from '~/content_editor/services/markdown_serializer';
describe('ContentEditor', () => {
let wrapper;
+ let editor;
- const buildWrapper = () => {
- wrapper = shallowMount(ContentEditor);
+ const buildWrapper = async () => {
+ editor = await createEditor({ serializer: createMarkdownSerializer({ toHTML: () => '' }) });
+ wrapper = shallowMount(ContentEditor, {
+ propsData: {
+ editor,
+ },
+ });
};
afterEach(() => {
wrapper.destroy();
});
- it('renders editor content component and attaches editor instance', () => {
- const editor = {};
+ it('renders editor content component and attaches editor instance', async () => {
+ await buildWrapper();
- createEditor.mockReturnValueOnce(editor);
- buildWrapper();
expect(wrapper.findComponent(EditorContent).props().editor).toBe(editor);
});
+
+ it('renders top toolbar component and attaches editor instance', async () => {
+ await buildWrapper();
+
+ expect(wrapper.findComponent(TopToolbar).props().editor).toBe(editor);
+ });
});
diff --git a/spec/frontend/content_editor/components/toolbar_button_spec.js b/spec/frontend/content_editor/components/toolbar_button_spec.js
new file mode 100644
index 00000000000..bef61fa08cb
--- /dev/null
+++ b/spec/frontend/content_editor/components/toolbar_button_spec.js
@@ -0,0 +1,85 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import ToolbarButton from '~/content_editor/components/toolbar_button.vue';
+
+describe('content_editor/components/toolbar_button', () => {
+ let wrapper;
+ let editor;
+ const CONTENT_TYPE = 'bold';
+ const ICON_NAME = 'bold';
+ const LABEL = 'Bold';
+
+ const buildEditor = () => {
+ editor = {
+ isActive: {
+ [CONTENT_TYPE]: jest.fn(),
+ },
+ commands: {
+ [CONTENT_TYPE]: jest.fn(),
+ },
+ };
+ };
+
+ const buildWrapper = (propsData = {}) => {
+ wrapper = shallowMount(ToolbarButton, {
+ stubs: {
+ GlButton,
+ },
+ propsData: {
+ editor,
+ contentType: CONTENT_TYPE,
+ iconName: ICON_NAME,
+ label: LABEL,
+ ...propsData,
+ },
+ });
+ };
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ buildEditor();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays tertiary, small button with a provided label and icon', () => {
+ buildWrapper();
+
+ expect(findButton().html()).toMatchSnapshot();
+ });
+
+ it.each`
+ editorState | outcomeDescription | outcome
+ ${{ isActive: true, focused: true }} | ${'button is active'} | ${true}
+ ${{ isActive: false, focused: true }} | ${'button is not active'} | ${false}
+ ${{ isActive: true, focused: false }} | ${'button is not active '} | ${false}
+ `('$outcomeDescription when when editor state is $editorState', ({ editorState, outcome }) => {
+ editor.isActive[CONTENT_TYPE].mockReturnValueOnce(editorState.isActive);
+ editor.focused = editorState.focused;
+ buildWrapper();
+
+ expect(findButton().classes().includes('active')).toBe(outcome);
+ });
+
+ describe('when button is clicked', () => {
+ it('executes the content type command when executeCommand = true', async () => {
+ buildWrapper({ executeCommand: true });
+
+ await findButton().trigger('click');
+
+ expect(editor.commands[CONTENT_TYPE]).toHaveBeenCalled();
+ expect(wrapper.emitted().click).toHaveLength(1);
+ });
+
+ it('does not executes the content type command when executeCommand = false', async () => {
+ buildWrapper({ executeCommand: false });
+
+ await findButton().trigger('click');
+
+ expect(editor.commands[CONTENT_TYPE]).not.toHaveBeenCalled();
+ expect(wrapper.emitted().click).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js
new file mode 100644
index 00000000000..6b04546ab4f
--- /dev/null
+++ b/spec/frontend/content_editor/components/top_toolbar_spec.js
@@ -0,0 +1,46 @@
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import TopToolbar from '~/content_editor/components/top_toolbar.vue';
+
+describe('content_editor/components/top_toolbar', () => {
+ let wrapper;
+ let editor;
+
+ const buildEditor = () => {
+ editor = {};
+ };
+
+ const buildWrapper = () => {
+ wrapper = extendedWrapper(
+ shallowMount(TopToolbar, {
+ propsData: {
+ editor,
+ },
+ }),
+ );
+ };
+
+ beforeEach(() => {
+ buildEditor();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ testId | button
+ ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold' }}
+ ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic' }}
+ ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code' }}
+ ${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote' }}
+ ${'bullet-list'} | ${{ contentType: 'bullet_list', iconName: 'list-bulleted', label: 'Add a bullet list' }}
+ ${'ordered-list'} | ${{ contentType: 'ordered_list', iconName: 'list-numbered', label: 'Add a numbered list' }}
+ `('renders $testId button', ({ testId, buttonProps }) => {
+ buildWrapper();
+ expect(wrapper.findByTestId(testId).props()).toMatchObject({
+ ...buttonProps,
+ editor,
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/services/create_editor_spec.js b/spec/frontend/content_editor/services/create_editor_spec.js
index 4cf63e608eb..a6a2327a11f 100644
--- a/spec/frontend/content_editor/services/create_editor_spec.js
+++ b/spec/frontend/content_editor/services/create_editor_spec.js
@@ -5,14 +5,24 @@ import createMarkdownSerializer from '~/content_editor/services/markdown_seriali
jest.mock('~/content_editor/services/markdown_serializer');
describe('content_editor/services/create_editor', () => {
+ const renderMarkdown = () => true;
const buildMockSerializer = () => ({
serialize: jest.fn(),
deserialize: jest.fn(),
});
+ it('sets gl-py-4 gl-px-5 class selectors to editor attributes', async () => {
+ const editor = await createEditor({ renderMarkdown });
+
+ expect(editor.options.editorProps).toMatchObject({
+ attributes: {
+ class: 'gl-py-4 gl-px-5',
+ },
+ });
+ });
+
describe('creating an editor', () => {
it('uses markdown serializer when a renderMarkdown function is provided', async () => {
- const renderMarkdown = () => true;
const mockSerializer = buildMockSerializer();
createMarkdownSerializer.mockReturnValueOnce(mockSerializer);
diff --git a/spec/lib/gitlab/hook_data/project_builder_spec.rb b/spec/lib/gitlab/hook_data/project_builder_spec.rb
new file mode 100644
index 00000000000..672dbab918f
--- /dev/null
+++ b/spec/lib/gitlab/hook_data/project_builder_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::HookData::ProjectBuilder do
+ let_it_be(:user) { create(:user, name: 'John', email: 'john@example.com') }
+ let_it_be(:namespace) { create(:namespace, owner: user) }
+ let_it_be(:project) { create(:project, :internal, name: 'my_project', namespace: namespace) }
+
+ describe '#build' do
+ let(:data) { described_class.new(project).build(event) }
+ let(:event_name) { data[:event_name] }
+ let(:attributes) do
+ [
+ :event_name, :created_at, :updated_at, :name, :path, :path_with_namespace, :project_id,
+ :owner_name, :owner_email, :project_visibility
+ ]
+ end
+
+ context 'data' do
+ shared_examples_for 'includes the required attributes' do
+ it 'includes the required attributes' do
+ expect(data).to include(*attributes)
+
+ expect(data[:created_at]).to eq(project.created_at.xmlschema)
+ expect(data[:updated_at]).to eq(project.updated_at.xmlschema)
+ expect(data[:name]).to eq('my_project')
+ expect(data[:path]).to eq(project.path)
+ expect(data[:path_with_namespace]).to eq(project.full_path)
+ expect(data[:project_id]).to eq(project.id)
+ expect(data[:owner_name]).to eq('John')
+ expect(data[:owner_email]).to eq('john@example.com')
+ expect(data[:project_visibility]).to eq('internal')
+ end
+ end
+
+ shared_examples_for 'does not include `old_path_with_namespace` attribute' do
+ it 'does not include `old_path_with_namespace` attribute' do
+ expect(data).not_to include(:old_path_with_namespace)
+ end
+ end
+
+ shared_examples_for 'includes `old_path_with_namespace` attribute' do
+ it 'includes `old_path_with_namespace` attribute' do
+ allow(project).to receive(:old_path_with_namespace).and_return('old-path-with-namespace')
+ expect(data[:old_path_with_namespace]).to eq('old-path-with-namespace')
+ end
+ end
+
+ context 'on create' do
+ let(:event) { :create }
+
+ it { expect(event_name).to eq('project_create') }
+ it_behaves_like 'includes the required attributes'
+ it_behaves_like 'does not include `old_path_with_namespace` attribute'
+ end
+
+ context 'on destroy' do
+ let(:event) { :destroy }
+
+ it { expect(event_name).to eq('project_destroy') }
+ it_behaves_like 'includes the required attributes'
+ it_behaves_like 'does not include `old_path_with_namespace` attribute'
+ end
+
+ context 'on rename' do
+ let(:event) { :rename }
+
+ it { expect(event_name).to eq('project_rename') }
+ it_behaves_like 'includes the required attributes'
+ it_behaves_like 'includes `old_path_with_namespace` attribute'
+ end
+
+ context 'on transfer' do
+ let(:event) { :transfer }
+
+ it { expect(event_name).to eq('project_transfer') }
+ it_behaves_like 'includes the required attributes'
+ it_behaves_like 'includes `old_path_with_namespace` attribute'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
index 7d8e3056384..db141358d4f 100644
--- a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
@@ -71,56 +71,6 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi
end
end
- context 'with AND operator' do
- let(:aggregated_metrics) do
- params = { source: datasource, operator: "AND", time_frame: time_frame }
- [
- aggregated_metric(**params.merge(name: "gmau_1", events: %w[event3 event5])),
- aggregated_metric(**params.merge(name: "gmau_2"))
- ]
- end
-
- it 'returns the number of unique events recorded for every metric in aggregate', :aggregate_failures do
- results = {
- 'gmau_1' => 2,
- 'gmau_2' => 1
- }
- params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at }
-
- # gmau_1 data is as follow
- # |A| => 4
- expect(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event3')).and_return(4)
- # |B| => 6
- expect(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event5')).and_return(6)
- # |A + B| => 8
- expect(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event3 event5])).and_return(8)
- # Exclusion inclusion principle formula to calculate intersection of 2 sets
- # |A & B| = (|A| + |B|) - |A + B| => (4 + 6) - 8 => 2
-
- # gmau_2 data is as follow:
- # |A| => 2
- expect(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event1')).and_return(2)
- # |B| => 3
- expect(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event2')).and_return(3)
- # |C| => 5
- expect(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event3')).and_return(5)
-
- # |A + B| => 4 therefore |A & B| = (|A| + |B|) - |A + B| => 2 + 3 - 4 => 1
- expect(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2])).and_return(4)
- # |A + C| => 6 therefore |A & C| = (|A| + |C|) - |A + C| => 2 + 5 - 6 => 1
- expect(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event3])).and_return(6)
- # |B + C| => 7 therefore |B & C| = (|B| + |C|) - |B + C| => 3 + 5 - 7 => 1
- expect(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event2 event3])).and_return(7)
- # |A + B + C| => 8
- expect(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2 event3])).and_return(8)
- # Exclusion inclusion principle formula to calculate intersection of 3 sets
- # |A & B & C| = (|A & B| + |A & C| + |B & C|) - (|A| + |B| + |C|) + |A + B + C|
- # (1 + 1 + 1) - (2 + 3 + 5) + 8 => 1
-
- expect(aggregated_metrics_data).to eq(results)
- end
- end
-
context 'with OR operator' do
let(:aggregated_metrics) do
[
@@ -331,36 +281,6 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi
it_behaves_like 'database_sourced_aggregated_metrics'
it_behaves_like 'redis_sourced_aggregated_metrics'
it_behaves_like 'db sourced aggregated metrics without database_sourced_aggregated_metrics feature'
-
- context 'metrics union calls' do
- it 'caches intermediate operations', :aggregate_failures do
- events = %w[event1 event2 event3 event5]
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:aggregated_metrics)
- .and_return([aggregated_metric(name: 'gmau_1', events: events, operator: "AND", time_frame: time_frame)])
- end
-
- params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at }
-
- events.each do |event|
- expect(sources::RedisHll).to receive(:calculate_metrics_union)
- .with(params.merge(metric_names: event))
- .once
- .and_return(0)
- end
-
- 2.upto(4) do |subset_size|
- events.combination(subset_size).each do |events|
- expect(sources::RedisHll).to receive(:calculate_metrics_union)
- .with(params.merge(metric_names: events))
- .once
- .and_return(0)
- end
- end
-
- aggregated_metrics_data
- end
- end
end
end
end
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/sources/calculations/intersection_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/sources/calculations/intersection_spec.rb
new file mode 100644
index 00000000000..41cb445155e
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/aggregates/sources/calculations/intersection_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::Calculations::Intersection do
+ let_it_be(:recorded_at) { Time.current.to_i }
+ let_it_be(:start_date) { 4.weeks.ago.to_date }
+ let_it_be(:end_date) { Date.current }
+
+ shared_examples 'aggregated_metrics_data with source' do
+ context 'with AND operator' do
+ let(:params) { { start_date: start_date, end_date: end_date, recorded_at: recorded_at } }
+
+ context 'with even number of metrics' do
+ it 'calculates intersection correctly', :aggregate_failures do
+ # gmau_1 data is as follow
+ # |A| => 4
+ expect(source).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event3')).and_return(4)
+ # |B| => 6
+ expect(source).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event5')).and_return(6)
+ # |A + B| => 8
+ expect(source).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event3 event5])).and_return(8)
+ # Exclusion inclusion principle formula to calculate intersection of 2 sets
+ # |A & B| = (|A| + |B|) - |A + B| => (4 + 6) - 8 => 2
+ expect(source.calculate_metrics_intersections(metric_names: %w[event3 event5], start_date: start_date, end_date: end_date, recorded_at: recorded_at)).to eq(2)
+ end
+ end
+
+ context 'with odd number of metrics' do
+ it 'calculates intersection correctly', :aggregate_failures do
+ # gmau_2 data is as follow:
+ # |A| => 2
+ expect(source).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event1')).and_return(2)
+ # |B| => 3
+ expect(source).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event2')).and_return(3)
+ # |C| => 5
+ expect(source).to receive(:calculate_metrics_union).with(params.merge(metric_names: 'event3')).and_return(5)
+
+ # |A + B| => 4 therefore |A & B| = (|A| + |B|) - |A + B| => 2 + 3 - 4 => 1
+ expect(source).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2])).and_return(4)
+ # |A + C| => 6 therefore |A & C| = (|A| + |C|) - |A + C| => 2 + 5 - 6 => 1
+ expect(source).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event3])).and_return(6)
+ # |B + C| => 7 therefore |B & C| = (|B| + |C|) - |B + C| => 3 + 5 - 7 => 1
+ expect(source).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event2 event3])).and_return(7)
+ # |A + B + C| => 8
+ expect(source).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2 event3])).and_return(8)
+ # Exclusion inclusion principle formula to calculate intersection of 3 sets
+ # |A & B & C| = (|A & B| + |A & C| + |B & C|) - (|A| + |B| + |C|) + |A + B + C|
+ # (1 + 1 + 1) - (2 + 3 + 5) + 8 => 1
+ expect(source.calculate_metrics_intersections(metric_names: %w[event1 event2 event3], start_date: start_date, end_date: end_date, recorded_at: recorded_at)).to eq(1)
+ end
+ end
+ end
+ end
+
+ describe '.aggregated_metrics_data' do
+ let(:source) do
+ Class.new do
+ extend Gitlab::Usage::Metrics::Aggregates::Sources::Calculations::Intersection
+ end
+ end
+
+ it 'caches intermediate operations', :aggregate_failures do
+ events = %w[event1 event2 event3 event5]
+
+ params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at }
+
+ events.each do |event|
+ expect(source).to receive(:calculate_metrics_union)
+ .with(params.merge(metric_names: event))
+ .once
+ .and_return(0)
+ end
+
+ 2.upto(4) do |subset_size|
+ events.combination(subset_size).each do |events|
+ expect(source).to receive(:calculate_metrics_union)
+ .with(params.merge(metric_names: events))
+ .once
+ .and_return(0)
+ end
+ end
+
+ expect(source.calculate_metrics_intersections(metric_names: events, start_date: start_date, end_date: end_date, recorded_at: recorded_at)).to eq(0)
+ end
+
+ it_behaves_like 'aggregated_metrics_data with source'
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb
index db878828cd6..1ae4c9414dd 100644
--- a/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb
@@ -12,11 +12,7 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::PostgresHll, :clean_
let(:metric_2) { 'metric_2' }
let(:metric_names) { [metric_1, metric_2] }
- describe '.calculate_events_union' do
- subject(:calculate_metrics_union) do
- described_class.calculate_metrics_union(metric_names: metric_names, start_date: start_date, end_date: end_date, recorded_at: recorded_at)
- end
-
+ describe 'metric calculations' do
before do
[
{
@@ -36,23 +32,55 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::PostgresHll, :clean_
end
end
- it 'returns the number of unique events in the union of all metrics' do
- expect(calculate_metrics_union.round(2)).to eq(3.12)
- end
+ describe '.calculate_events_union' do
+ subject(:calculate_metrics_union) do
+ described_class.calculate_metrics_union(metric_names: metric_names, start_date: start_date, end_date: end_date, recorded_at: recorded_at)
+ end
+
+ it 'returns the number of unique events in the union of all metrics' do
+ expect(calculate_metrics_union.round(2)).to eq(3.12)
+ end
+
+ context 'when there is no aggregated data saved' do
+ let(:metric_names) { [metric_1, 'i do not have any records'] }
+
+ it 'raises error when union data is missing' do
+ expect { calculate_metrics_union }.to raise_error Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable
+ end
+ end
- context 'when there is no aggregated data saved' do
- let(:metric_names) { [metric_1, 'i do not have any records'] }
+ context 'when there is only one metric defined as aggregated' do
+ let(:metric_names) { [metric_1] }
- it 'raises error when union data is missing' do
- expect { calculate_metrics_union }.to raise_error Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable
+ it 'returns the number of unique events for that metric' do
+ expect(calculate_metrics_union.round(2)).to eq(2.08)
+ end
end
end
- context 'when there is only one metric defined as aggregated' do
- let(:metric_names) { [metric_1] }
+ describe '.calculate_metrics_intersections' do
+ subject(:calculate_metrics_intersections) do
+ described_class.calculate_metrics_intersections(metric_names: metric_names, start_date: start_date, end_date: end_date, recorded_at: recorded_at)
+ end
+
+ it 'returns the number of common events in the intersection of all metrics' do
+ expect(calculate_metrics_intersections.round(2)).to eq(1.04)
+ end
+
+ context 'when there is no aggregated data saved' do
+ let(:metric_names) { [metric_1, 'i do not have any records'] }
- it 'returns the number of unique events for that metric' do
- expect(calculate_metrics_union.round(2)).to eq(2.08)
+ it 'raises error when union data is missing' do
+ expect { calculate_metrics_intersections }.to raise_error Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable
+ end
+ end
+
+ context 'when there is only one metric defined in aggregate' do
+ let(:metric_names) { [metric_1] }
+
+ it 'returns the number of common/unique events for the intersection of that metric' do
+ expect(calculate_metrics_intersections.round(2)).to eq(2.08)
+ end
end
end
end
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb
index af2de5ea343..83b155b41b1 100644
--- a/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb
@@ -3,11 +3,12 @@
require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::RedisHll do
- describe '.calculate_events_union' do
- let(:event_names) { %w[event_a event_b] }
- let(:start_date) { 7.days.ago }
- let(:end_date) { Date.current }
+ let_it_be(:event_names) { %w[event_a event_b] }
+ let_it_be(:start_date) { 7.days.ago }
+ let_it_be(:end_date) { Date.current }
+ let_it_be(:recorded_at) { Time.current }
+ describe '.calculate_events_union' do
subject(:calculate_metrics_union) do
described_class.calculate_metrics_union(metric_names: event_names, start_date: start_date, end_date: end_date, recorded_at: nil)
end
@@ -26,4 +27,30 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Sources::RedisHll do
expect { calculate_metrics_union }.to raise_error Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable
end
end
+
+ describe '.calculate_metrics_intersections' do
+ subject(:calculate_metrics_intersections) do
+ described_class.calculate_metrics_intersections(metric_names: event_names, start_date: start_date, end_date: end_date, recorded_at: recorded_at)
+ end
+
+ it 'uses values returned by union to compute the intersection' do
+ event_names.each do |event|
+ expect(Gitlab::Usage::Metrics::Aggregates::Sources::RedisHll).to receive(:calculate_metrics_union)
+ .with(metric_names: event, start_date: start_date, end_date: end_date, recorded_at: recorded_at)
+ .and_return(5)
+ end
+
+ expect(Gitlab::Usage::Metrics::Aggregates::Sources::RedisHll).to receive(:calculate_metrics_union)
+ .with(metric_names: event_names, start_date: start_date, end_date: end_date, recorded_at: recorded_at)
+ .and_return(2)
+
+ expect(calculate_metrics_intersections).to eq(8)
+ end
+
+ it 'raises error if union is < 0' do
+ allow(Gitlab::Usage::Metrics::Aggregates::Sources::RedisHll).to receive(:calculate_metrics_union).and_raise(Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable)
+
+ expect { calculate_metrics_intersections }.to raise_error(Gitlab::Usage::Metrics::Aggregates::Sources::UnionNotAvailable)
+ end
+ end
end