Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-22 21:09:50 +00:00
parent 3ce55b46df
commit f05ceb978a
83 changed files with 949 additions and 1909 deletions

View File

@ -1336,11 +1336,12 @@ export default class Notes {
toggleCommitList(e) {
const $element = $(e.currentTarget);
const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
const $svgChevronUpElement = $element.find('svg.js-chevron-up');
const $svgChevronDownElement = $element.find('svg.js-chevron-down');
$svgChevronUpElement.toggleClass('gl-display-none');
$svgChevronDownElement.toggleClass('gl-display-none');
$element
.find('.fa')
.toggleClass('fa-angle-down')
.toggleClass('fa-angle-up');
$closestSystemCommitList.toggleClass('hide-shade');
}

View File

@ -71,22 +71,14 @@ export default {
>
<div class="table-section section-20 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div>
<div
v-gl-tooltip
:title="testCase.classname"
class="table-mobile-content pr-md-1 text-truncate"
>
<div class="table-mobile-content pr-md-1 gl-overflow-wrap-break">
{{ testCase.classname }}
</div>
</div>
<div class="table-section section-20 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div>
<div
v-gl-tooltip
:title="testCase.name"
class="table-mobile-content pr-md-1 text-truncate"
>
<div class="table-mobile-content pr-md-1 gl-overflow-wrap-break">
{{ testCase.name }}
</div>
</div>

View File

@ -6,7 +6,8 @@ class SearchController < ApplicationController
include RendersCommits
SCOPE_PRELOAD_METHOD = {
projects: :with_web_entity_associations
projects: :with_web_entity_associations,
issues: :with_web_entity_associations
}.freeze
around_action :allow_gitaly_ref_name_caching

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
# This model is not yet intended to be used.
# It is in a transitioning phase while we are partitioning
# the table on the database-side.
# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/3206
# for details.
class AuditEventPartitioned < ApplicationRecord
include PartitionedTable
self.table_name = 'audit_events_part_5fc467ac26'
partitioned_by :created_at, strategy: :monthly
end

View File

@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
VERSION = '0.19.0'
VERSION = '0.19.1'
self.table_name = 'clusters_applications_runners'

View File

@ -87,6 +87,8 @@ class Issue < ApplicationRecord
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
scope :with_web_entity_associations, -> { preload(:author, :project) }
scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) }
scope :with_label_attributes, ->(label_attributes) { joins(:labels).where(labels: label_attributes) }
scope :with_alert_management_alerts, -> { joins(:alert_management_alert) }
scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) }

View File

@ -1,15 +0,0 @@
.banner-callout.compact.milestone-deprecation-message.js-milestone-deprecation-message.prepend-top-20
.banner-graphic= image_tag 'illustrations/milestone_removing-page.svg'
.banner-body.gl-ml-3.gl-mr-3
%h5.banner-title.gl-mt-0= _('This page will be removed in a future release.')
%p.milestone-banner-text= _('Use group milestones to manage issues from multiple projects in the same milestone.')
= button_tag _('Promote these project milestones into a group milestone.'), class: 'btn btn-link js-popover-link text-align-left milestone-banner-link'
.milestone-banner-buttons.prepend-top-20= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-default', target: '_blank'
%template.js-milestone-deprecation-message-template
.milestone-popover-body
%ol.milestone-popover-instructions-list.gl-mb-0
%li= _('Click any <strong>project name</strong> in the project list below to navigate to the project milestone.').html_safe
%li= _('Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone.').html_safe
%hr.popover-hr
.milestone-popover-footer= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-link prepend-left-0', target: '_blank'

View File

@ -63,7 +63,8 @@
- if note.system
.system-note-commit-list-toggler.hide
= _("Toggle commit list")
%i.fa.fa-angle-down
= sprite_icon('chevron-down', size: 16, css_class: 'js-chevron-down gl-ml-1 gl-vertical-align-text-bottom')
= sprite_icon('chevron-up', size: 16, css_class: 'js-chevron-up gl-ml-1 gl-vertical-align-text-bottom gl-display-none')
- if note.attachment.url
.note-attachment
- if note.attachment.image?

View File

@ -0,0 +1,5 @@
---
title: Replace fa-angle-up icons with GitLab SVG
merge_request: 36429
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Avoid N+1 of issue associations in Search
merge_request: 36941
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Move service desk usage data to core
merge_request: 37080
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Store user mentions from merge request title or description in the DB
merge_request: 34378
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Update GitLab Runner Helm Chart to 0.19.1
merge_request: 37583
author:
type: other

View File

@ -20,11 +20,44 @@ Gitlab.ee do
end
end
# When running on multi-threaded runtimes like Puma or Sidekiq,
# set the number of threads per process as the minimum DB connection pool size.
# This is to avoid connectivity issues as was documented here:
# https://github.com/rails/rails/pull/23057
if Gitlab::Runtime.multi_threaded?
# TODO get rid of feature flag https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/495
if ENV['DB_USE_NEW_POOL_SIZE_LOGIC'] == '1'
# Because of the way Ruby on Rails manages database connections, it is
# important that we have at least as many connections as we have
# threads. While there is a 'pool' setting in database.yml, it is not
# very practical because you need to maintain it in tandem with the
# number of application threads. Because of this we override the number
# of allowed connections in the database connection pool based on the
# configured number of application threads.
#
# Gitlab::Runtime.max_threads is the number of "user facing" application
# threads the process has been configured with. We also have auxiliary
# threads that use database connections. Because it is not practical to
# keep an accurate count of the number auxiliary threads as the
# application evolves over time, we just add a fixed headroom to the
# number of user-facing threads. It is OK if this number is too large
# because connections are instantiated lazily.
headroom = (ENV["DB_POOL_HEADROOM"].presence || 10).to_i
calculated_pool_size = Gitlab::Runtime.max_threads + headroom
db_config = Gitlab::Database.config ||
Rails.application.config.database_configuration[Rails.env]
db_config['pool'] = calculated_pool_size
ActiveRecord::Base.establish_connection(db_config)
Gitlab.ee do
if Gitlab::Runtime.sidekiq? && Gitlab::Geo.geo_database_configured?
Rails.configuration.geo_database['pool'] = calculated_pool_size
Geo::TrackingBase.establish_connection(Rails.configuration.geo_database)
end
end
elsif Gitlab::Runtime.multi_threaded?
# When running on multi-threaded runtimes like Puma or Sidekiq,
# set the number of threads per process as the minimum DB connection pool size.
# This is to avoid connectivity issues as was documented here:
# https://github.com/rails/rails/pull/23057
max_threads = Gitlab::Runtime.max_threads
db_config = Gitlab::Database.config ||
Rails.application.config.database_configuration[Rails.env]

View File

@ -3,8 +3,10 @@
# Make sure we have loaded partitioned models here
# (even with eager loading disabled).
Gitlab::Database::Partitioning::PartitionCreator.register(AuditEventPartitioned)
begin
Gitlab::Database::Partitioning::PartitionCreator.new.create_partitions
Gitlab::Database::Partitioning::PartitionCreator.new.create_partitions unless ENV['DISABLE_POSTGRES_PARTITION_CREATION_ON_STARTUP']
rescue ActiveRecord::ActiveRecordError, PG::Error
# ignore - happens when Rake tasks yet have to create a database, e.g. for testing
end

View File

@ -42,23 +42,29 @@ OPTIONAL_REVIEW_TEMPLATE = "%{role} review is optional for %{category}".freeze
NOT_AVAILABLE_TEMPLATE = 'No %{role} available'.freeze
TIMEZONE_EXPERIMENT = true
def mr_author
roulette.team.find { |person| person.username == gitlab.mr_author }
def note_for_spins_role(spins, role)
spins.each do |spin|
note = note_for_spin_role(spin, role)
return note if note
end
NOT_AVAILABLE_TEMPLATE % { role: role }
end
def note_for_category_role(spin, role)
def note_for_spin_role(spin, role)
if spin.optional_role == role
return OPTIONAL_REVIEW_TEMPLATE % { role: role.capitalize, category: helper.label_for_category(spin.category) }
end
spin.public_send(role)&.markdown_name(timezone_experiment: TIMEZONE_EXPERIMENT, author: mr_author) || NOT_AVAILABLE_TEMPLATE % { role: role } # rubocop:disable GitlabSecurity/PublicSend
spin.public_send(role)&.markdown_name(timezone_experiment: spin.timezone_experiment, author: roulette.team_mr_author) # rubocop:disable GitlabSecurity/PublicSend
end
def markdown_row_for_spin(spin)
reviewer_note = note_for_category_role(spin, :reviewer)
maintainer_note = note_for_category_role(spin, :maintainer)
def markdown_row_for_spins(category, spins_array)
reviewer_note = note_for_spins_role(spins_array, :reviewer)
maintainer_note = note_for_spins_role(spins_array, :maintainer)
"| #{helper.label_for_category(spin.category)} | #{reviewer_note} | #{maintainer_note} |"
"| #{helper.label_for_category(category)} | #{reviewer_note} | #{maintainer_note} |"
end
changes = helper.changes_by_category
@ -70,26 +76,20 @@ changes.delete(:docs)
categories = changes.keys - [:unknown]
# Ensure to spin for database reviewer/maintainer when ~database is applied (e.g. to review SQL queries)
categories << :database if gitlab.mr_labels.include?('database') && !categories.include?(:database)
categories << :database if helper.gitlab_helper&.mr_labels&.include?('database') && !categories.include?(:database)
if changes.any?
project = helper.project_name
branch_name = gitlab.mr_json['source_branch']
markdown(MESSAGE)
timezone_roulette_spins = roulette.spin(project, categories, timezone_experiment: true)
random_roulette_spins = roulette.spin(project, categories, timezone_experiment: false)
roulette_spins = roulette.spin(project, categories, branch_name, timezone_experiment: TIMEZONE_EXPERIMENT)
rows = roulette_spins.map do |spin|
# MR includes QA changes, but also other changes, and author isn't an SET
if spin.category == :qa && categories.size > 1 && mr_author && !mr_author.reviewer?(project, spin.category, [])
spin.optional_role = :maintainer
end
spin.optional_role = :maintainer if spin.category == :test
markdown_row_for_spin(spin)
rows = timezone_roulette_spins.map do |spin|
fallback_spin = random_roulette_spins.find { |random_roulette_spins| random_roulette_spins.category == spin.category }
markdown_row_for_spins(spin.category, [spin, fallback_spin])
end
markdown(MESSAGE)
markdown(CATEGORY_TABLE_HEADER + rows.join("\n")) unless rows.empty?
unknown = changes.fetch(:unknown, [])

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class PartitionAuditEvents < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
include Gitlab::Database::PartitioningMigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
partition_table_by_date :audit_events, :created_at
end
def down
drop_partitioned_table_for :audit_events
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
class MigrateAllMergeRequestUserMentionsToDb < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
DELAY = 2.minutes.to_i
BATCH_SIZE = 100_000
MIGRATION = 'UserMentions::CreateResourceUserMention'
JOIN = "LEFT JOIN merge_request_user_mentions on merge_requests.id = merge_request_user_mentions.merge_request_id"
QUERY_CONDITIONS = "(description LIKE '%@%' OR title LIKE '%@%') AND merge_request_user_mentions.merge_request_id IS NULL"
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
end
def up
delay = DELAY
MergeRequest.each_batch(of: BATCH_SIZE) do |batch, _|
range = batch.pluck('MIN(merge_requests.id)', 'MAX(merge_requests.id)').first
records_count = MergeRequest.joins(JOIN).where(QUERY_CONDITIONS).where(id: range.first..range.last).count
if records_count > 0
migrate_in(delay, MIGRATION, ['MergeRequest', JOIN, QUERY_CONDITIONS, false, *range])
delay += [DELAY, (records_count / 500 + 1).minutes.to_i].max
end
end
end
def down
# no-op
end
end

View File

@ -12,6 +12,78 @@ CREATE EXTENSION IF NOT EXISTS btree_gist WITH SCHEMA public;
CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;
CREATE FUNCTION public.table_sync_function_2be879775d() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF (TG_OP = 'DELETE') THEN
DELETE FROM audit_events_part_5fc467ac26 where id = OLD.id;
ELSIF (TG_OP = 'UPDATE') THEN
UPDATE audit_events_part_5fc467ac26
SET author_id = NEW.author_id,
type = NEW.type,
entity_id = NEW.entity_id,
entity_type = NEW.entity_type,
details = NEW.details,
updated_at = NEW.updated_at,
ip_address = NEW.ip_address,
author_name = NEW.author_name,
entity_path = NEW.entity_path,
target_details = NEW.target_details,
created_at = NEW.created_at
WHERE audit_events_part_5fc467ac26.id = NEW.id;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO audit_events_part_5fc467ac26 (id,
author_id,
type,
entity_id,
entity_type,
details,
updated_at,
ip_address,
author_name,
entity_path,
target_details,
created_at)
VALUES (NEW.id,
NEW.author_id,
NEW.type,
NEW.entity_id,
NEW.entity_type,
NEW.details,
NEW.updated_at,
NEW.ip_address,
NEW.author_name,
NEW.entity_path,
NEW.target_details,
NEW.created_at);
END IF;
RETURN NULL;
END
$$;
COMMENT ON FUNCTION public.table_sync_function_2be879775d() IS 'Partitioning migration: table sync for audit_events table';
CREATE TABLE public.audit_events_part_5fc467ac26 (
id bigint NOT NULL,
author_id integer NOT NULL,
type character varying NOT NULL,
entity_id integer NOT NULL,
entity_type character varying NOT NULL,
details text,
updated_at timestamp without time zone,
ip_address inet,
author_name text,
entity_path text,
target_details text,
created_at timestamp without time zone NOT NULL,
CONSTRAINT check_492aaa021d CHECK ((char_length(entity_path) <= 5500)),
CONSTRAINT check_83ff8406e2 CHECK ((char_length(author_name) <= 255)),
CONSTRAINT check_d493ec90b5 CHECK ((char_length(target_details) <= 5500))
)
PARTITION BY RANGE (created_at);
CREATE TABLE public.product_analytics_events_experimental (
id bigint NOT NULL,
project_id integer NOT NULL,
@ -17443,6 +17515,9 @@ ALTER TABLE ONLY public.approvers
ALTER TABLE ONLY public.ar_internal_metadata
ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key);
ALTER TABLE ONLY public.audit_events_part_5fc467ac26
ADD CONSTRAINT audit_events_part_5fc467ac26_pkey PRIMARY KEY (id, created_at);
ALTER TABLE ONLY public.audit_events
ADD CONSTRAINT audit_events_pkey PRIMARY KEY (id);
@ -21054,6 +21129,8 @@ ALTER INDEX public.product_analytics_events_experimental_pkey ATTACH PARTITION g
ALTER INDEX public.product_analytics_events_experimental_pkey ATTACH PARTITION gitlab_partitions_static.product_analytics_events_experimental_63_pkey;
CREATE TRIGGER table_sync_trigger_ee39a25f9d AFTER INSERT OR DELETE OR UPDATE ON public.audit_events FOR EACH ROW EXECUTE PROCEDURE public.table_sync_function_2be879775d();
ALTER TABLE ONLY public.chat_names
ADD CONSTRAINT fk_00797a2bf9 FOREIGN KEY (service_id) REFERENCES public.services(id) ON DELETE CASCADE;
@ -23860,6 +23937,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200528123703
20200528125905
20200528171933
20200601120434
20200601210148
20200602013900
20200602013901
@ -23995,6 +24073,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200715135130
20200715202659
20200716044023
20200716120000
20200716120419
20200716145156
20200718040100

View File

@ -1,6 +1,6 @@
---
stage: Manage
group: Analytics
group: Compliance
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/#designated-technical-writers
---

View File

@ -0,0 +1,31 @@
---
stage: Manage
group: Compliance
description: 'Learn how to create evidence artifacts typically requested by a 3rd party auditor.'
---
# Audit Reports
GitLab can help owners and administrators respond to auditors by generating
comprehensive reports. These **Audit Reports** vary in scope, depending on the
need:
## Use cases
- Generate a report of audit events to provide to an external auditor requesting proof of certain logging capabilities.
- Provide a report of all users showing their group and project memberships for a quarterly access review so the auditor can verify compliance with an organization's access management policy.
## APIs
- `https://docs.gitlab.com/ee/api/audit_events.html`
- `https://docs.gitlab.com/ee/api/graphql/reference/#user`
- `https://docs.gitlab.com/ee/api/graphql/reference/#groupmember`
- `https://docs.gitlab.com/ee/api/graphql/reference/#projectmember`
## Features
- `https://docs.gitlab.com/ee/administration/audit_events.html`
- `https://docs.gitlab.com/ee/administration/logs.html`
We plan on making Audit Events [downloadable as a CSV](https://gitlab.com/gitlab-org/gitlab/-/issues/1449)
in the near future.

View File

@ -5054,6 +5054,11 @@ type Group {
"""
id: ID!
"""
Status of the temporary storage increase
"""
isTemporaryStorageIncreaseEnabled: Boolean!
"""
Issues of the group
"""
@ -8316,6 +8321,11 @@ type Namespace {
"""
id: ID!
"""
Status of the temporary storage increase
"""
isTemporaryStorageIncreaseEnabled: Boolean!
"""
Indicates if Large File Storage (LFS) is enabled for namespace
"""

View File

@ -14079,6 +14079,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "isTemporaryStorageIncreaseEnabled",
"description": "Status of the temporary storage increase",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issues",
"description": "Issues of the group",
@ -24824,6 +24842,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "isTemporaryStorageIncreaseEnabled",
"description": "Status of the temporary storage increase",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "lfsEnabled",
"description": "Indicates if Large File Storage (LFS) is enabled for namespace",

View File

@ -821,6 +821,7 @@ Autogenerated return type of EpicTreeReorder
| `fullPath` | ID! | Full path of the namespace |
| `groupTimelogsEnabled` | Boolean | Indicates if Group timelogs are enabled for namespace |
| `id` | ID! | ID of the namespace |
| `isTemporaryStorageIncreaseEnabled` | Boolean! | Status of the temporary storage increase |
| `label` | Label | A label available on this group |
| `lfsEnabled` | Boolean | Indicates if Large File Storage (LFS) is enabled for namespace |
| `mentionsDisabled` | Boolean | Indicates if a group is disabled from getting mentioned |
@ -1278,6 +1279,7 @@ Contains statistics about a milestone
| `fullName` | String! | Full name of the namespace |
| `fullPath` | ID! | Full path of the namespace |
| `id` | ID! | ID of the namespace |
| `isTemporaryStorageIncreaseEnabled` | Boolean! | Status of the temporary storage increase |
| `lfsEnabled` | Boolean | Indicates if Large File Storage (LFS) is enabled for namespace |
| `name` | String! | Name of the namespace |
| `path` | String! | Path of the namespace |

View File

@ -1,18 +1,5 @@
---
comments: false
redirect_to: '../topics/index.md'
---
# Technical articles list (deprecated)
Technical articles are
topic-related documentation, written with a user-friendly approach and language, aiming
to provide the community with guidance on specific processes to achieve certain objectives.
The list of technical articles was [deprecated](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/41138) in favor of having them linked from their topic-related documentation:
- [Git](../topics/git/index.md)
- [GitLab administrator](../administration/index.md)
- [GitLab CI/CD](../ci/README.md)
- [GitLab Pages](../user/project/pages/index.md)
- [GitLab user](../user/index.md)
- [Install GitLab](../install/README.md)
This document was moved to [another location](../topics/index.md)

View File

@ -27,7 +27,7 @@ be used. There is no need to add a title called "Introduction" or "Overview," be
search for these terms. Just put this information after the title.
- **Use cases**: describes real use case scenarios for that feature/configuration.
- **Requirements**: describes what software, configuration, account, or knowledge is required.
- **Instructions**: One or more sets of detailed instructions to follow.
- **Instructions**: one or more sets of detailed instructions to follow.
- **Troubleshooting** guide (recommended but not required).
For additional details on each, see the [template for new docs](#template-for-new-docs),
@ -167,3 +167,23 @@ Disqus, therefore, don't add both keys to the same document.
The click events in the feedback section are tracked with Google Tag Manager. The
conversions can be viewed on Google Analytics by navigating to **Behavior > Events > Top events > docs**.
## Guidelines for good practices
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36576/) in GitLab 13.2 as GitLab Development documentation.
'Good practice' examples demonstrate encouraged ways of writing code while comparing with examples of practices to avoid.
These examples are labeled as "Bad" or "Good".
In GitLab development guidelines, when presenting the cases, it is recommended
to follow a **first-bad-then-good** strategy. First demonstrate the "Bad" practice (how things _could_ be done, which is often still working code),
and then how things _should_ be done better, using a "Good" example. This is typically an improved example of the same code.
Consider the following guidelines when offering examples:
- First, offer the "Bad" example, then the "Good" one.
- When only one bad case and one good case is given, use the same code block.
- When more than one bad case or one good case is offered, use separated code blocks for each.
With many examples being presented, a clear separation helps the reader to go directly to the good part.
Consider offering an explanation (for example, a comment, a link to a resource, etc.) on why something is bad practice.
- Better and best cases can be considered part of the good case(s) code block.
In the same code block, precede each with comments: `# Better` and `# Best`.

View File

@ -170,22 +170,14 @@ Some more examples can be found in the [Frontend unit tests section](testing_lev
Another common gotcha is that the specs end up verifying the mock is working. If you are using mocks, the mock should support the test, but not be the target of the test.
**Bad:**
```javascript
const spy = jest.spyOn(idGenerator, 'create')
spy.mockImplementation = () = '1234'
// Bad
expect(idGenerator.create()).toBe('1234')
```
**Good:**
```javascript
const spy = jest.spyOn(idGenerator, 'create')
spy.mockImplementation = () = '1234'
// Actually focusing on the logic of your component and just leverage the controllable mocks output
// Good: actually focusing on the logic of your component and just leverage the controllable mocks output
expect(wrapper.find('div').html()).toBe('<div id="1234">...</div>')
```
@ -212,22 +204,22 @@ Preferentially, in component testing with `@vue/test-utils`, you should query fo
- A `data-testid` attribute ([recommended by maintainers of `@vue/test-utils`](https://github.com/vuejs/vue-test-utils/issues/1498#issuecomment-610133465))
- a Vue `ref` (if using `@vue/test-utils`)
Examples:
```javascript
// Bad
it('exists', () => {
// Good
wrapper.find(FooComponent);
wrapper.find('input[name=foo]');
wrapper.find('[data-testid="foo"]');
wrapper.find({ ref: 'foo'});
// Bad
wrapper.find('.js-foo');
wrapper.find('.btn-primary');
wrapper.find('.qa-foo-component');
wrapper.find('[data-qa-selector="foo"]');
});
// Good
it('exists', () => {
wrapper.find(FooComponent);
wrapper.find('input[name=foo]');
wrapper.find('[data-testid="foo"]');
wrapper.find({ ref: 'foo'});
});
```
It is not recommended that you add `.js-*` classes just for testing purposes. Only do this if there are no other feasible options available.
@ -239,22 +231,15 @@ Do not use a `.qa-*` class or `data-qa-selector` attribute for any tests other t
When writing describe test blocks to test specific functions/methods,
please use the method name as the describe block name.
```javascript
// Good
describe('methodName', () => {
it('passes', () => {
expect(true).toEqual(true);
});
});
**Bad**:
// Bad
```javascript
describe('#methodName', () => {
it('passes', () => {
expect(true).toEqual(true);
});
});
// Bad
describe('.methodName', () => {
it('passes', () => {
expect(true).toEqual(true);
@ -262,6 +247,16 @@ describe('.methodName', () => {
});
```
**Good**:
```javascript
describe('methodName', () => {
it('passes', () => {
expect(true).toEqual(true);
});
});
```
### Testing promises
When testing Promises you should always make sure that the test is asynchronous and rejections are handled. It's now possible to use the `async/await` syntax in the test suite:
@ -286,36 +281,17 @@ it('tests a promise rejection', async () => {
You can also work with Promise chains. In this case, you can make use of the `done` callback and `done.fail` in case an error occurred. Following are some examples:
**Bad**:
```javascript
// Good
it('tests a promise', done => {
promise
.then(data => {
expect(data).toBe(asExpected);
})
.then(done)
.catch(done.fail);
});
// Good
it('tests a promise rejection', done => {
promise
.then(done.fail)
.catch(error => {
expect(error).toBe(expectedError);
})
.then(done)
.catch(done.fail);
});
// Bad (missing done callback)
// missing done callback
it('tests a promise', () => {
promise.then(data => {
expect(data).toBe(asExpected);
});
});
// Bad (missing catch)
// missing catch
it('tests a promise', done => {
promise
.then(data => {
@ -324,7 +300,7 @@ it('tests a promise', done => {
.then(done);
});
// Bad (use done.fail in asynchronous tests)
// use done.fail in asynchronous tests
it('tests a promise', done => {
promise
.then(data => {
@ -334,7 +310,7 @@ it('tests a promise', done => {
.catch(fail);
});
// Bad (missing catch)
// missing catch
it('tests a promise rejection', done => {
promise
.catch(error => {
@ -344,6 +320,31 @@ it('tests a promise rejection', done => {
});
```
**Good**:
```javascript
// handling success
it('tests a promise', done => {
promise
.then(data => {
expect(data).toBe(asExpected);
})
.then(done)
.catch(done.fail);
});
// failure case
it('tests a promise rejection', done => {
promise
.then(done.fail)
.catch(error => {
expect(error).toBe(expectedError);
})
.then(done)
.catch(done.fail);
});
```
### Manipulating Time
Sometimes we have to test time-sensitive code. For example, recurring events that run every X amount of seconds or similar. Here you'll find some strategies to deal with that:
@ -564,11 +565,11 @@ Examples:
```javascript
const foo = 1;
// good
expect(foo).toBe(1);
// bad
// Bad
expect(foo).toEqual(1);
// Good
expect(foo).toBe(1);
```
#### Prefer more befitting matchers
@ -621,12 +622,11 @@ Jest has the tricky `toBeDefined` matcher that can produce false positive test.
the given value for `undefined` only.
```javascript
// good
expect(wrapper.find('foo').exists()).toBe(true);
// bad
// if finder returns null, the test will pass
// Bad: if finder returns null, the test will pass
expect(wrapper.find('foo')).toBeDefined();
// Good
expect(wrapper.find('foo').exists()).toBe(true);
```
#### Avoid using `setImmediate`

View File

@ -69,9 +69,11 @@ It can be set to:
## Feature flag strategies
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35555) in GitLab 13.0.
> - It's deployed behind a feature flag, disabled by default.
> - It was deployed behind a feature flag, disabled by default.
> - It became [enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/214684) in GitLab 13.2.
> - It's recommended for production use.
> - It's enabled on GitLab.com.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-feature-flag-strategies). **(CORE ONLY)**
> - For GitLab self-managed instances, a GitLab administrator can choose to [disable it](#enable-or-disable-feature-flag-strategies). **(CORE ONLY)**
You can apply a feature flag strategy across multiple environments, without defining
the strategy multiple times.
@ -140,23 +142,23 @@ activation strategy.
### Enable or disable feature flag strategies
This feature is under development, but is ready for testing. It's
This feature is under development, but is ready for production use. It's
deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
can disable it for your instance.
To enable it:
```ruby
Feature.enable(:feature_flags_new_version)
```
To disable it:
```ruby
Feature.disable(:feature_flags_new_version)
```
To enable it:
```ruby
Feature.enable(:feature_flags_new_version)
```
## Disable a feature flag for a specific environment
In [GitLab 13.0 and earlier](https://gitlab.com/gitlab-org/gitlab/-/issues/8621),

View File

@ -4,9 +4,9 @@ group: APM
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/#designated-technical-writers
---
# Using Variables **(CORE)**
# Using variables **(CORE)**
## Query Variables
## Query variables
Variables can be specified using double curly braces, such as `"{{ci_environment_slug}}"` ([added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20793) in GitLab 12.7).
@ -41,7 +41,7 @@ For example, if the dashboard time range is set to 8 hours, the value of
[Variables can be defined](../../../operations/metrics/dashboards/yaml.md#templating-templating-properties) in a custom dashboard YAML file.
## Query Variables from URL
## Query variables from URL
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214500) in GitLab 13.0.

View File

@ -4,7 +4,7 @@ group: APM
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/#designated-technical-writers
---
# Monitor metrics for your CI/CD environment **(CORE)**
# Metrics dashboard for your CI/CD environment **(CORE)**
After [configuring Prometheus for a cluster](../../user/project/integrations/prometheus.md),
GitLab attempts to retrieve performance metrics for any [environment](../../ci/environments/index.md) with

View File

@ -390,7 +390,9 @@ Here are the requirements for using Dependency Scanning in an offline environmen
- Keep Docker-In-Docker disabled (default).
- GitLab Runner with the [`docker` or `kubernetes` executor](#requirements).
- Docker Container Registry with locally available copies of Dependency Scanning [analyzer](https://gitlab.com/gitlab-org/security-products/analyzers) images.
- Host an offline Git copy of the [gemnasium-db advisory database](https://gitlab.com/gitlab-org/security-products/gemnasium-db/)
- Host an offline Git copy of the [gemnasium-db advisory database](https://gitlab.com/gitlab-org/security-products/gemnasium-db/).
This is required because in an offline environment, the Gemnasium analyzer can't fetch the latest
advisories from the online repository.
- _Only if scanning Ruby projects_: Host an offline Git copy of the [advisory database](https://github.com/rubysec/ruby-advisory-db).
- _Only if scanning npm/yarn projects_: Host an offline copy of the [retire.js](https://github.com/RetireJS/retire.js/) [node](https://github.com/RetireJS/retire.js/blob/master/repository/npmrepository.json) and [js](https://github.com/RetireJS/retire.js/blob/master/repository/jsrepository.json) advisory databases.
@ -428,8 +430,10 @@ For details on saving and transporting Docker images as a file, see Docker's doc
### Set Dependency Scanning CI job variables to use local Dependency Scanning analyzers
Add the following configuration to your `.gitlab-ci.yml` file. You must replace
`SECURE_ANALYZERS_PREFIX` to refer to your local Docker container registry:
Add the following configuration to your `.gitlab-ci.yml` file. You must change the value of
`SECURE_ANALYZERS_PREFIX` to refer to your local Docker container registry. You must also change the
value of `GEMNASIUM_DB_REMOTE_URL` to the location of your offline Git copy of the
[gemnasium-db advisory database](https://gitlab.com/gitlab-org/security-products/gemnasium-db/):
```yaml
include:

View File

@ -51,7 +51,7 @@ A pipeline consists of multiple jobs, including SAST and DAST scanning. If any j
## Requirements
To run SAST jobs, by default, you need GitLab Runner with the
To run SAST jobs, by default, you need a GitLab Runner with the
[`docker`](https://docs.gitlab.com/runner/executors/docker.html) or
[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html) executor.
If you're using the shared Runners on GitLab.com, this is enabled by default.
@ -208,7 +208,7 @@ Read more on [how to use private Maven repositories](../index.md#using-private-m
If needed, you can enable Docker-in-Docker to restore the SAST behavior that existed prior to GitLab
13.0. Follow these steps to do so:
1. Configure GitLab Runner with Docker-inDocker in [privileged mode](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode).
1. Configure a GitLab Runner with Docker-in-Docker in [privileged mode](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode).
1. Set the variable `SAST_DISABLE_DIND` set to `false`:
```yaml
@ -503,8 +503,8 @@ run successfully. For more information, see [Offline environments](../offline_de
To use SAST in an offline environment, you need:
- To keep Docker-In-Docker disabled (default).
- GitLab Runner with the [`docker` or `kubernetes` executor](#requirements).
- Docker Container Registry with locally available copies of SAST [analyzer](https://gitlab.com/gitlab-org/security-products/analyzers) images.
- A GitLab Runner with the [`docker` or `kubernetes` executor](#requirements).
- A Docker Container Registry with locally available copies of SAST [analyzer](https://gitlab.com/gitlab-org/security-products/analyzers) images.
NOTE: **Note:**
GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy),

View File

@ -4,7 +4,7 @@ group: Health
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/#designated-technical-writers
---
# Incident Management
# Incident management
GitLab offers solutions for handling incidents in your applications and services,
such as setting up Prometheus alerts, displaying metrics, and sending notifications.

View File

@ -4,7 +4,7 @@ group: APM
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/#designated-technical-writers
---
# Monitoring AWS Resources
# Monitoring AWS resources
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12621) in GitLab 9.4

View File

@ -4,7 +4,7 @@ group: Health
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/#designated-technical-writers
---
# Alert Management
# Alert management
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2877) in GitLab 13.0.

View File

@ -4,7 +4,7 @@ group: APM
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/#designated-technical-writers
---
# Metrics dashboard settings
# Dashboard settings
You can configure your [Monitoring dashboard](../integrations/prometheus.md) to
display the time zone of your choice, and the links of your choice.

View File

@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/169) in GitLab 11.8.
Error tracking allows developers to easily discover and view the errors that their application may be generating. By surfacing error information where the code is being developed, efficiency and awareness can be increased.
Error Tracking allows developers to easily discover and view the errors that their application may be generating. By surfacing error information where the code is being developed, efficiency and awareness can be increased.
## Sentry error tracking

View File

@ -1,21 +0,0 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class ArchiveLegacyTraces
def perform(start_id, stop_id)
# This background migration directly refers to ::Ci::Build model which is defined in application code.
# In general, migration code should be isolated as much as possible in order to be idempotent.
# However, `archive!` method is too complicated to be replicated by coping its subsequent code.
# So we chose a way to use ::Ci::Build directly and we don't change the `archive!` method until 11.1
::Ci::Build.finished.without_archived_trace
.where(id: start_id..stop_id).find_each do |build|
build.trace.archive!
rescue => e
Rails.logger.error "Failed to archive live trace. id: #{build.id} message: #{e.message}" # rubocop:disable Gitlab/RailsLogger
end
end
end
end
end

View File

@ -1,15 +0,0 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Class that will fill the project_repositories table for projects that
# are on hashed storage and an entry is is missing in this table.
class BackfillHashedProjectRepositories < BackfillProjectRepositories
private
def projects
Project.on_hashed_storage
end
end
end
end

View File

@ -1,213 +0,0 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# This module is used to write the full path of all projects to
# the git repository config file.
# Storing the full project path in the git config allows admins to
# easily identify a project when it is using hashed storage.
module BackfillProjectFullpathInRepoConfig
OrphanedNamespaceError = Class.new(StandardError)
module Storage
# Class that returns the disk path for a project using hashed storage
class Hashed
attr_accessor :project
ROOT_PATH_PREFIX = '@hashed'
def initialize(project)
@project = project
end
def disk_path
"#{ROOT_PATH_PREFIX}/#{disk_hash[0..1]}/#{disk_hash[2..3]}/#{disk_hash}"
end
def disk_hash
@disk_hash ||= Digest::SHA2.hexdigest(project.id.to_s) if project.id
end
end
# Class that returns the disk path for a project using legacy storage
class LegacyProject
attr_accessor :project
def initialize(project)
@project = project
end
def disk_path
project.full_path
end
end
end
# Concern used by Project and Namespace to determine the full
# route to the project
module Routable
extend ActiveSupport::Concern
def full_path
@full_path ||= build_full_path
end
def build_full_path
return path unless has_parent?
raise OrphanedNamespaceError if parent.nil?
parent.full_path + '/' + path
end
def has_parent?
read_attribute(association(:parent).reflection.foreign_key)
end
end
# Class used to interact with repository using Gitaly
class Repository
attr_reader :storage
def initialize(storage, relative_path)
@storage = storage
@relative_path = relative_path
end
def gitaly_repository
Gitaly::Repository.new(storage_name: @storage, relative_path: @relative_path)
end
end
# Namespace can be a user or group. It can be the root or a
# child of another namespace.
class Namespace < ActiveRecord::Base
self.table_name = 'namespaces'
self.inheritance_column = nil
include Routable
belongs_to :parent, class_name: 'Namespace', inverse_of: 'namespaces'
has_many :projects, inverse_of: :parent
has_many :namespaces, inverse_of: :parent
end
# Project is where the repository (etc.) is stored
class Project < ActiveRecord::Base
self.table_name = 'projects'
include Routable
include EachBatch
FULLPATH_CONFIG_KEY = 'gitlab.fullpath'
belongs_to :parent, class_name: 'Namespace', foreign_key: :namespace_id, inverse_of: 'projects'
delegate :disk_path, to: :storage
def add_fullpath_config
entries = { FULLPATH_CONFIG_KEY => full_path }
repository_service.set_config(entries)
end
def remove_fullpath_config
repository_service.delete_config([FULLPATH_CONFIG_KEY])
end
def cleanup_repository
repository_service.cleanup
end
def storage
@storage ||=
if hashed_storage?
Storage::Hashed.new(self)
else
Storage::LegacyProject.new(self)
end
end
def hashed_storage?
self.storage_version && self.storage_version >= 1
end
def repository
@repository ||= Repository.new(repository_storage, disk_path + '.git')
end
def repository_service
@repository_service ||= Gitlab::GitalyClient::RepositoryService.new(repository)
end
end
# Base class for Up and Down migration classes
class BackfillFullpathMigration
RETRY_DELAY = 15.minutes
MAX_RETRIES = 2
# Base class for retrying one project
class BaseRetryOne
def perform(project_id, retry_count)
project = Project.find(project_id)
return unless project
migration_class.new.safe_perform_one(project, retry_count)
end
end
def perform(start_id, end_id)
Project.includes(:parent).where(id: start_id..end_id).each do |project|
safe_perform_one(project)
end
end
def safe_perform_one(project, retry_count = 0)
perform_one(project)
rescue GRPC::NotFound, GRPC::InvalidArgument, OrphanedNamespaceError
nil
rescue GRPC::BadStatus
schedule_retry(project, retry_count + 1) if retry_count < MAX_RETRIES
end
def schedule_retry(project, retry_count)
# Constants provided to BackgroundMigrationWorker must be within the
# scope of Gitlab::BackgroundMigration
retry_class_name = self.class::RetryOne.name.sub('Gitlab::BackgroundMigration::', '')
BackgroundMigrationWorker.perform_in(RETRY_DELAY, retry_class_name, [project.id, retry_count])
end
end
# Class to add the fullpath to the git repo config
class Up < BackfillFullpathMigration
# Class used to retry
class RetryOne < BaseRetryOne
def migration_class
Up
end
end
def perform_one(project)
project.cleanup_repository
project.add_fullpath_config
end
end
# Class to rollback adding the fullpath to the git repo config
class Down < BackfillFullpathMigration
# Class used to retry
class RetryOne < BaseRetryOne
def migration_class
Down
end
end
def perform_one(project)
project.cleanup_repository
project.remove_fullpath_config
end
end
end
end
end

View File

@ -1,19 +0,0 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class FillFileStoreJobArtifact
class JobArtifact < ActiveRecord::Base
self.table_name = 'ci_job_artifacts'
end
def perform(start_id, stop_id)
FillFileStoreJobArtifact::JobArtifact
.where(file_store: nil)
.where(id: (start_id..stop_id))
.update_all(file_store: 1)
end
end
end
end

View File

@ -1,19 +0,0 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class FillFileStoreLfsObject
class LfsObject < ActiveRecord::Base
self.table_name = 'lfs_objects'
end
def perform(start_id, stop_id)
FillFileStoreLfsObject::LfsObject
.where(file_store: nil)
.where(id: (start_id..stop_id))
.update_all(file_store: 1)
end
end
end
end

View File

@ -1,20 +0,0 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class FillStoreUpload
class Upload < ActiveRecord::Base
self.table_name = 'uploads'
self.inheritance_column = :_type_disabled
end
def perform(start_id, stop_id)
FillStoreUpload::Upload
.where(store: nil)
.where(id: (start_id..stop_id))
.update_all(store: 1)
end
end
end
end

View File

@ -1,140 +0,0 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class FixCrossProjectLabelLinks
GROUP_NESTED_LEVEL = 10.freeze
class Project < ActiveRecord::Base
self.table_name = 'projects'
end
class Label < ActiveRecord::Base
self.inheritance_column = :_type_disabled
self.table_name = 'labels'
end
class LabelLink < ActiveRecord::Base
self.table_name = 'label_links'
end
class Issue < ActiveRecord::Base
self.table_name = 'issues'
end
class MergeRequest < ActiveRecord::Base
self.table_name = 'merge_requests'
end
class Namespace < ActiveRecord::Base
self.inheritance_column = :_type_disabled
self.table_name = 'namespaces'
def self.groups_with_descendants_ids(start_id, stop_id)
# To isolate migration code, we avoid usage of
# Gitlab::GroupHierarchy#base_and_descendants which already
# does this job better
ids = Namespace.where(type: 'Group', id: Label.where(type: 'GroupLabel').select('distinct group_id')).where(id: start_id..stop_id).pluck(:id)
group_ids = ids
GROUP_NESTED_LEVEL.times do
ids = Namespace.where(type: 'Group', parent_id: ids).pluck(:id)
break if ids.empty?
group_ids += ids
end
group_ids.uniq
end
end
def perform(start_id, stop_id)
group_ids = Namespace.groups_with_descendants_ids(start_id, stop_id)
project_ids = Project.where(namespace_id: group_ids).select(:id)
fix_issues(project_ids)
fix_merge_requests(project_ids)
end
private
# select IDs of issues which reference a label which is:
# a) a project label of a different project, or
# b) a group label of a different group than issue's project group
def fix_issues(project_ids)
issue_ids = Label
.joins('INNER JOIN label_links ON label_links.label_id = labels.id AND label_links.target_type = \'Issue\'
INNER JOIN issues ON issues.id = label_links.target_id
INNER JOIN projects ON projects.id = issues.project_id')
.where('issues.project_id in (?)', project_ids)
.where('(labels.project_id is not null and labels.project_id != issues.project_id) '\
'or (labels.group_id is not null and labels.group_id != projects.namespace_id)')
.select('distinct issues.id')
Issue.where(id: issue_ids).find_each { |issue| check_resource_labels(issue, issue.project_id) }
end
# select IDs of MRs which reference a label which is:
# a) a project label of a different project, or
# b) a group label of a different group than MR's project group
def fix_merge_requests(project_ids)
mr_ids = Label
.joins('INNER JOIN label_links ON label_links.label_id = labels.id AND label_links.target_type = \'MergeRequest\'
INNER JOIN merge_requests ON merge_requests.id = label_links.target_id
INNER JOIN projects ON projects.id = merge_requests.target_project_id')
.where('merge_requests.target_project_id in (?)', project_ids)
.where('(labels.project_id is not null and labels.project_id != merge_requests.target_project_id) '\
'or (labels.group_id is not null and labels.group_id != projects.namespace_id)')
.select('distinct merge_requests.id')
MergeRequest.where(id: mr_ids).find_each { |merge_request| check_resource_labels(merge_request, merge_request.target_project_id) }
end
def check_resource_labels(resource, project_id)
local_labels = available_labels(project_id)
# get all label links for the given resource (issue/MR)
# which reference a label not included in available_labels
# (other than its project labels and labels of ancestor groups)
cross_labels = LabelLink
.select('label_id, labels.title as title, labels.color as color, label_links.id as label_link_id')
.joins('INNER JOIN labels ON labels.id = label_links.label_id')
.where(target_type: resource.class.name.demodulize, target_id: resource.id)
.where('labels.id not in (?)', local_labels.select(:id))
cross_labels.each do |label|
matching_label = local_labels.find {|l| l.title == label.title && l.color == label.color}
next unless matching_label
Rails.logger.info "#{resource.class.name.demodulize} #{resource.id}: replacing #{label.label_id} with #{matching_label.id}" # rubocop:disable Gitlab/RailsLogger
LabelLink.update(label.label_link_id, label_id: matching_label.id)
end
end
# get all labels available for the project (including
# group labels of ancestor groups)
def available_labels(project_id)
@labels ||= {}
@labels[project_id] ||= Label
.where("(type = 'GroupLabel' and group_id in (?)) or (type = 'ProjectLabel' and id = ?)",
project_group_ids(project_id),
project_id)
end
def project_group_ids(project_id)
ids = [Project.find(project_id).namespace_id]
GROUP_NESTED_LEVEL.times do
group = Namespace.find(ids.last)
break unless group.parent_id
ids << group.parent_id
end
ids
end
end
end
end

View File

@ -1,48 +0,0 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class MigrateBuildStage
module Migratable
class Stage < ActiveRecord::Base
self.table_name = 'ci_stages'
end
class Build < ActiveRecord::Base
self.table_name = 'ci_builds'
self.inheritance_column = :_type_disabled
def ensure_stage!(attempts: 2)
find_stage || create_stage!
rescue ActiveRecord::RecordNotUnique
retry if (attempts -= 1) > 0
raise
end
def find_stage
Stage.find_by(name: self.stage || 'test',
pipeline_id: self.commit_id,
project_id: self.project_id)
end
def create_stage!
Stage.create!(name: self.stage || 'test',
pipeline_id: self.commit_id,
project_id: self.project_id)
end
end
end
def perform(start_id, stop_id)
stages = Migratable::Build.where('stage_id IS NULL')
.where('id BETWEEN ? AND ?', start_id, stop_id)
.map { |build| build.ensure_stage! }
.compact.map(&:id)
MigrateBuildStageIdReference.new.perform(start_id, stop_id)
MigrateStageStatus.new.perform(stages.min, stages.max)
end
end
end
end

View File

@ -1,22 +0,0 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class MigrateBuildStageIdReference
def perform(start_id, stop_id)
sql = <<-SQL.strip_heredoc
UPDATE ci_builds
SET stage_id =
(SELECT id FROM ci_stages
WHERE ci_stages.pipeline_id = ci_builds.commit_id
AND ci_stages.name = ci_builds.stage)
WHERE ci_builds.id BETWEEN #{start_id.to_i} AND #{stop_id.to_i}
AND ci_builds.stage_id IS NULL
SQL
ActiveRecord::Base.connection.execute(sql)
end
end
end
end

View File

@ -1,35 +0,0 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class MigrateStageIndex
def perform(start_id, stop_id)
migrate_stage_index_sql(start_id.to_i, stop_id.to_i).tap do |sql|
ActiveRecord::Base.connection.execute(sql)
end
end
private
def migrate_stage_index_sql(start_id, stop_id)
<<~SQL
WITH freqs AS (
SELECT stage_id, stage_idx, COUNT(*) AS freq FROM ci_builds
WHERE stage_id BETWEEN #{start_id} AND #{stop_id}
AND stage_idx IS NOT NULL
GROUP BY stage_id, stage_idx
), indexes AS (
SELECT DISTINCT stage_id, first_value(stage_idx)
OVER (PARTITION BY stage_id ORDER BY freq DESC) AS index
FROM freqs
)
UPDATE ci_stages SET position = indexes.index
FROM indexes WHERE indexes.stage_id = ci_stages.id
AND ci_stages.position IS NULL;
SQL
end
end
end
end

View File

@ -1,82 +0,0 @@
# frozen_string_literal: true
#
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class PopulateClusterKubernetesNamespaceTable
include Gitlab::Database::MigrationHelpers
BATCH_SIZE = 1_000
module Migratable
class KubernetesNamespace < ActiveRecord::Base
self.table_name = 'clusters_kubernetes_namespaces'
end
class ClusterProject < ActiveRecord::Base
include EachBatch
self.table_name = 'cluster_projects'
belongs_to :project
def self.with_no_kubernetes_namespace
where.not(id: Migratable::KubernetesNamespace.select(:cluster_project_id))
end
def namespace
slug = "#{project.path}-#{project.id}".downcase
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
def service_account
"#{namespace}-service-account"
end
end
class Project < ActiveRecord::Base
self.table_name = 'projects'
end
end
def perform
cluster_projects_with_no_kubernetes_namespace.each_batch(of: BATCH_SIZE) do |cluster_projects_batch, index|
sql_values = sql_values_for(cluster_projects_batch)
insert_into_cluster_kubernetes_namespace(sql_values)
end
end
private
def cluster_projects_with_no_kubernetes_namespace
Migratable::ClusterProject.with_no_kubernetes_namespace
end
def sql_values_for(cluster_projects)
cluster_projects.map do |cluster_project|
values_for_cluster_project(cluster_project)
end
end
def values_for_cluster_project(cluster_project)
{
cluster_project_id: cluster_project.id,
cluster_id: cluster_project.cluster_id,
project_id: cluster_project.project_id,
namespace: cluster_project.namespace,
service_account_name: cluster_project.service_account,
created_at: 'NOW()',
updated_at: 'NOW()'
}
end
def insert_into_cluster_kubernetes_namespace(rows)
Gitlab::Database.bulk_insert(Migratable::KubernetesNamespace.table_name, # rubocop:disable Gitlab/BulkInsert
rows,
disable_quote: [:created_at, :updated_at])
end
end
end
end

View File

@ -1,158 +0,0 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
# rubocop:disable Metrics/ClassLength
module Gitlab
module BackgroundMigration
class RemoveRestrictedTodos
PRIVATE_FEATURE = 10
PRIVATE_PROJECT = 0
class Project < ActiveRecord::Base
self.table_name = 'projects'
end
class ProjectAuthorization < ActiveRecord::Base
self.table_name = 'project_authorizations'
end
class ProjectFeature < ActiveRecord::Base
self.table_name = 'project_features'
end
class Todo < ActiveRecord::Base
include EachBatch
self.table_name = 'todos'
end
class Issue < ActiveRecord::Base
include EachBatch
self.table_name = 'issues'
end
def perform(start_id, stop_id)
projects = Project.where('EXISTS (SELECT 1 FROM todos WHERE todos.project_id = projects.id)')
.where(id: start_id..stop_id)
projects.each do |project|
remove_confidential_issue_todos(project.id)
if project.visibility_level == PRIVATE_PROJECT
remove_non_members_todos(project.id)
else
remove_restricted_features_todos(project.id)
end
end
end
private
def remove_non_members_todos(project_id)
batch_remove_todos_cte(project_id)
end
def remove_confidential_issue_todos(project_id)
# min access level to access a confidential issue is reporter
min_reporters = authorized_users(project_id)
.select(:user_id)
.where('access_level >= ?', 20)
confidential_issues = Issue.select(:id, :author_id).where(confidential: true, project_id: project_id)
confidential_issues.each_batch(of: 100, order_hint: :confidential) do |batch|
batch.each do |issue|
assigned_users = IssueAssignee.select(:user_id).where(issue_id: issue.id)
todos = Todo.where(target_type: 'Issue', target_id: issue.id)
.where('user_id NOT IN (?)', min_reporters)
.where('user_id NOT IN (?)', assigned_users)
todos = todos.where('user_id != ?', issue.author_id) if issue.author_id
todos.delete_all
end
end
end
def remove_restricted_features_todos(project_id)
ProjectFeature.where(project_id: project_id).each do |project_features|
target_types = []
target_types << 'Issue' if private?(project_features.issues_access_level)
target_types << 'MergeRequest' if private?(project_features.merge_requests_access_level)
target_types << 'Commit' if private?(project_features.repository_access_level)
next if target_types.empty?
batch_remove_todos_cte(project_id, target_types)
end
end
def private?(feature_level)
feature_level == PRIVATE_FEATURE
end
def authorized_users(project_id)
ProjectAuthorization.select(:user_id).where(project_id: project_id)
end
def unauthorized_project_todos(project_id)
Todo.where(project_id: project_id)
.where('user_id NOT IN (?)', authorized_users(project_id))
end
def batch_remove_todos_cte(project_id, target_types = nil)
loop do
count = remove_todos_cte(project_id, target_types)
break if count == 0
end
end
def remove_todos_cte(project_id, target_types = nil)
sql = []
sql << with_all_todos_sql(project_id, target_types)
sql << as_deleted_sql
sql << "SELECT count(*) FROM deleted"
result = Todo.connection.exec_query(sql.join(' '))
result.rows[0][0].to_i
end
def with_all_todos_sql(project_id, target_types = nil)
if target_types
table = Arel::Table.new(:todos)
in_target = table[:target_type].in(target_types)
target_types_sql = " AND #{in_target.to_sql}"
end
<<-SQL
WITH all_todos AS (
SELECT id
FROM "todos"
WHERE "todos"."project_id" = #{project_id}
AND (user_id NOT IN (
SELECT "project_authorizations"."user_id"
FROM "project_authorizations"
WHERE "project_authorizations"."project_id" = #{project_id})
#{target_types_sql}
)
),
SQL
end
def as_deleted_sql
<<-SQL
deleted AS (
DELETE FROM todos
WHERE id IN (
SELECT id
FROM all_todos
LIMIT 5000
)
RETURNING id
)
SQL
end
end
end
end

View File

@ -1,26 +0,0 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
# Ensures services which previously received all notes events continue
# to receive confidential ones.
class SetConfidentialNoteEventsOnServices
class Service < ActiveRecord::Base
self.table_name = 'services'
include ::EachBatch
def self.services_to_update
where(confidential_note_events: nil, note_events: true)
end
end
def perform(start_id, stop_id)
Service.services_to_update
.where(id: start_id..stop_id)
.update_all(confidential_note_events: true)
end
end
end
end

View File

@ -1,26 +0,0 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
# Ensures hooks which previously received all notes events continue
# to receive confidential ones.
class SetConfidentialNoteEventsOnWebhooks
class WebHook < ActiveRecord::Base
self.table_name = 'web_hooks'
include ::EachBatch
def self.hooks_to_update
where(confidential_note_events: nil, note_events: true)
end
end
def perform(start_id, stop_id)
WebHook.hooks_to_update
.where(id: start_id..stop_id)
.update_all(confidential_note_events: true)
end
end
end
end

View File

@ -12,26 +12,22 @@ module Gitlab
ISOLATION_MODULE = 'Gitlab::BackgroundMigration::UserMentions::Models'
def perform(resource_model, join, conditions, with_notes, start_id, end_id)
return unless Feature.enabled?(:migrate_user_mentions, default_enabled: true)
resource_model = "#{ISOLATION_MODULE}::#{resource_model}".constantize if resource_model.is_a?(String)
model = with_notes ? Gitlab::BackgroundMigration::UserMentions::Models::Note : resource_model
resource_user_mention_model = resource_model.user_mention_model
records = model.joins(join).where(conditions).where(id: start_id..end_id)
records.in_groups_of(BULK_INSERT_SIZE, false).each do |records|
records.each_batch(of: BULK_INSERT_SIZE) do |records|
mentions = []
records.each do |record|
mention_record = record.build_mention_values(resource_user_mention_model.resource_foreign_key)
mentions << mention_record unless mention_record.blank?
end
Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert
resource_user_mention_model.table_name,
mentions,
return_ids: true,
disable_quote: resource_model.no_quote_columns,
on_conflict: :do_nothing
)
resource_user_mention_model.insert_all(mentions) unless mentions.empty?
end
end
end

View File

@ -6,6 +6,7 @@ module Gitlab
module UserMentions
module Models
class Commit
include EachBatch
include Concerns::IsolatedMentionable
include Concerns::MentionableMigrationMethods

View File

@ -7,6 +7,7 @@ module Gitlab
module Models
module DesignManagement
class Design < ActiveRecord::Base
include EachBatch
include Concerns::MentionableMigrationMethods
def self.user_mention_model

View File

@ -6,6 +6,7 @@ module Gitlab
module UserMentions
module Models
class Epic < ActiveRecord::Base
include EachBatch
include Concerns::IsolatedMentionable
include Concerns::MentionableMigrationMethods
include CacheMarkdownField

View File

@ -6,6 +6,7 @@ module Gitlab
module UserMentions
module Models
class MergeRequest < ActiveRecord::Base
include EachBatch
include Concerns::IsolatedMentionable
include CacheMarkdownField
include Concerns::MentionableMigrationMethods

View File

@ -6,6 +6,7 @@ module Gitlab
module UserMentions
module Models
class Note < ActiveRecord::Base
include EachBatch
include Concerns::IsolatedMentionable
include CacheMarkdownField

View File

@ -53,7 +53,7 @@ module Gitlab
def ee?
# Support former project name for `dev` and support local Danger run
%w[gitlab gitlab-ee].include?(ENV['CI_PROJECT_NAME']) || Dir.exist?('../../ee')
%w[gitlab gitlab-ee].include?(ENV['CI_PROJECT_NAME']) || Dir.exist?(File.expand_path('../../../ee', __dir__))
end
def gitlab_helper

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
require_relative 'teammate'
require_relative 'request_helper'
module Gitlab
module Danger
@ -12,45 +13,49 @@ module Gitlab
database: false
}.freeze
Spin = Struct.new(:category, :reviewer, :maintainer, :optional_role)
Spin = Struct.new(:category, :reviewer, :maintainer, :optional_role, :timezone_experiment)
def team_mr_author
team.find { |person| person.username == mr_author_username }
end
# Assigns GitLab team members to be reviewer and maintainer
# for each change category that a Merge Request contains.
#
# @return [Array<Spin>]
def spin(project, categories, branch_name, timezone_experiment: false)
team =
begin
project_team(project)
rescue => err
warn("Reviewer roulette failed to load team data: #{err.message}")
[]
end
canonical_branch_name = canonical_branch_name(branch_name)
spin_per_category = categories.each_with_object({}) do |category, memo|
def spin(project, categories, timezone_experiment: false)
spins = categories.map do |category|
including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(category, timezone_experiment)
memo[category] = spin_for_category(team, project, category, canonical_branch_name, timezone_experiment: including_timezone)
spin_for_category(project, category, timezone_experiment: including_timezone)
end
spin_per_category.map do |category, spin|
case category
backend_spin = spins.find { |spin| spin.category == :backend }
spins.each do |spin|
including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(spin.category, timezone_experiment)
case spin.category
when :qa
# MR includes QA changes, but also other changes, and author isn't an SET
if categories.size > 1 && !team_mr_author&.reviewer?(project, spin.category, [])
spin.optional_role = :maintainer
end
when :test
spin.optional_role = :maintainer
if spin.reviewer.nil?
# Fetch an already picked backend reviewer, or pick one otherwise
spin.reviewer = spin_per_category[:backend]&.reviewer || spin_for_category(team, project, :backend, canonical_branch_name).reviewer
spin.reviewer = backend_spin&.reviewer || spin_for_category(project, :backend, timezone_experiment: including_timezone).reviewer
end
when :engineering_productivity
if spin.maintainer.nil?
# Fetch an already picked backend maintainer, or pick one otherwise
spin.maintainer = spin_per_category[:backend]&.maintainer || spin_for_category(team, project, :backend, canonical_branch_name).maintainer
spin.maintainer = backend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
end
end
spin
end
spins
end
# Looks up the current list of GitLab team members and parses it into a
@ -73,14 +78,9 @@ module Gitlab
# @return [Array<Teammate>]
def project_team(project_name)
team.select { |member| member.in_project?(project_name) }
end
def canonical_branch_name(branch_name)
branch_name.gsub(/^[ce]e-|-[ce]e$/, '')
end
def new_random(seed)
Random.new(Digest::MD5.hexdigest(seed).to_i(16))
rescue => err
warn("Reviewer roulette failed to load team data: #{err.message}")
[]
end
# Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
@ -113,16 +113,35 @@ module Gitlab
# @param [Teammate] person
# @return [Boolean]
def mr_author?(person)
person.username == gitlab.mr_author
person.username == mr_author_username
end
def mr_author_username
helper.gitlab_helper&.mr_author || `whoami`
end
def mr_source_branch
return `git rev-parse --abbrev-ref HEAD` unless helper.gitlab_helper&.mr_json
helper.gitlab_helper.mr_json['source_branch']
end
def mr_labels
helper.gitlab_helper&.mr_labels || []
end
def new_random(seed)
Random.new(Digest::MD5.hexdigest(seed).to_i(16))
end
def spin_role_for_category(team, role, project, category)
team.select do |member|
member.public_send("#{role}?", project, category, gitlab.mr_labels) # rubocop:disable GitlabSecurity/PublicSend
member.public_send("#{role}?", project, category, mr_labels) # rubocop:disable GitlabSecurity/PublicSend
end
end
def spin_for_category(team, project, category, branch_name, timezone_experiment: false)
def spin_for_category(project, category, timezone_experiment: false)
team = project_team(project)
reviewers, traintainers, maintainers =
%i[reviewer traintainer maintainer].map do |role|
spin_role_for_category(team, role, project, category)
@ -132,11 +151,11 @@ module Gitlab
# https://gitlab.com/gitlab-org/gitlab/issues/26723
# Make traintainers have triple the chance to be picked as a reviewer
random = new_random(branch_name)
random = new_random(mr_source_branch)
reviewer = spin_for_person(reviewers + traintainers + traintainers, random: random, timezone_experiment: timezone_experiment)
maintainer = spin_for_person(maintainers, random: random, timezone_experiment: timezone_experiment)
Spin.new(category, reviewer, maintainer)
Spin.new(category, reviewer, maintainer, false, timezone_experiment)
end
end
end

View File

@ -3,10 +3,11 @@
module Gitlab
module Danger
class Teammate
attr_reader :username, :name, :role, :projects, :available, :tz_offset_hours
attr_reader :options, :username, :name, :role, :projects, :available, :tz_offset_hours
# The options data are produced by https://gitlab.com/gitlab-org/gitlab-roulette/-/blob/master/lib/team_member.rb
def initialize(options = {})
@options = options
@username = options['username']
@name = options['name']
@markdown_name = options['markdown_name']
@ -16,6 +17,16 @@ module Gitlab
@tz_offset_hours = options['tz_offset_hours']
end
def to_h
options
end
def ==(other)
return false unless other.respond_to?(:username)
other.username == username
end
def in_project?(name)
projects&.has_key?(name)
end

View File

@ -323,20 +323,6 @@
- "Choisissez entre <code>clone</code> ou <code>fetch</code> pour obtenir les dernières modifications du code de lapplication"
- "Elija entre <code>clone</code> o <code>fetch</code> para obtener el código de la aplicación más reciente"
- "Wybierz pomiędzy <code>klonem</code> lub <code>pobierz</code> aby uzyskać najnowszy kod aplikacji"
"Click any <strong>project name</strong> in the project list below to navigate to the project milestone.":
plural_id:
translations:
- "在專案列表點擊任何<strong>專案名稱</strong>,將轉跳到專案的里程碑。"
- "Clique em qualquer <strong>nome de projeto</strong> na lista a seguir para navegar para o marco do projeto."
- "プロジェクトリストで<strong>プロジェクト名</strong>をクリックすると、プロジェクトのマイルストーンに移動します。"
- "Clica em qualquer <strong>nome de projeto</strong> na lista a seguir para navegar para o objetivo do projeto."
- "Выберите из списка любой <strong>проект</strong>, чтобы перейти к этапу проекта."
- "单击下面项目列表中的任何 <strong>项目名称</strong> 跳转到项目里程碑。"
- "Клікніть по будь-якому <strong>імені проекту</strong> зі списку нижче для того, щоб перейти до етапу проекту."
- "Klicke auf einen beliebigen <strong>-Projektnamen</strong> in der folgenden Projektliste, um zum Projektmeilenstein zu navigieren."
- "아래 프로젝트 목록에서 <strong>프로젝트 이름</strong>을 눌러 프로젝트 마일스톤을 봅니다."
- "Cliquez sur nimporte quel <strong>nom de projet</strong> dans la liste des projets cidessous pour naviguer jusquau jalon du projet."
- "Haga clic en cualquier <strong>nombre de proyecto</strong> en la lista de proyectos que se muestra a continuación para navegar hasta el hito de proyecto correspondiente."
"Click the <strong>Download</strong> button and wait for downloading to complete.":
plural_id:
translations:
@ -350,19 +336,6 @@
- "Cliquez sur le bouton <strong>Télécharger</strong> et attendez que le téléchargement soit terminé."
- "Haga click en el botón <strong>Descargar</strong> y espere a que se complete la descarga."
- "<strong>İndirme</strong> düğmesini tıklayın ve indirme işleminin tamamlanmasını bekleyin."
"Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone.":
plural_id:
translations:
- "點擊左上角的<strong>提升</strong>按鈕,將提升至群組里程碑。"
- "Clique no botão <strong>Promover</strong> no canto superior direito para promover a um marco de grupo."
- "右上の<strong>昇格</strong>ボタンをクリックしてグループマイルストーンへ昇格"
- "Clica no botão <strong>Promover</strong> no canto superior direito para promovê-lo para um objetivo de grupo."
- "点击右上角的 <strong>升级</strong> 按钮以升级到到群组里程碑。"
- "Натисніть кнопку <strong>Перенести</strong> у правому верхному куті щоб перенести етап на рівень групи."
- "Klicke auf die Schaltfläche <strong>Hochstufen</strong> in der oberen rechten Ecke, um einen ihn zu einem Gruppenmeilenstein hochzustufen."
- "오른쪽 상단 모서리의 <strong>승격</strong> 단추를 눌러 그룹 마일스톤으로 보낼 수 있습니다."
- "Cliquez sur le bouton <strong>Promouvoir</strong> en haut à droite pour le promouvoir en tant que jalon de groupe."
- "Haga clic sobre el botón <strong>Promocionar</strong> , situado en la esquina superior derecha para promocionarlo a un hito de grupo."
"Click the <strong>Select none</strong> button on the right, since we only need \\\"Google Code Project Hosting\\\".":
plural_id:
translations:
@ -1046,3 +1019,37 @@
"DeployFreeze|Specify times when deployments are not allowed for an environment. The <code>gitlab-ci.yml</code> file must be updated to make deployment jobs aware of the %{freeze_period_link_start}freeze period%{freeze_period_link_end}.":
plural_id:
translations:
#
# Strings below are fixed in the source code but the translations are still present in CrowdIn so the
# locale files will fail the linter. They can be deleted after next CrowdIn sync, likely in:
# https://gitlab.com/gitlab-org/gitlab/-/issues/226008
#
"Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone.":
plural_id:
translations:
- "點擊左上角的<strong>提升</strong>按鈕,將提升至群組里程碑。"
- "Clique no botão <strong>Promover</strong> no canto superior direito para promover a um marco de grupo."
- "右上の<strong>昇格</strong>ボタンをクリックしてグループマイルストーンへ昇格"
- "Clica no botão <strong>Promover</strong> no canto superior direito para promovê-lo para um objetivo de grupo."
- "点击右上角的 <strong>升级</strong> 按钮以升级到到群组里程碑。"
- "Натисніть кнопку <strong>Перенести</strong> у правому верхному куті щоб перенести етап на рівень групи."
- "Klicke auf die Schaltfläche <strong>Hochstufen</strong> in der oberen rechten Ecke, um einen ihn zu einem Gruppenmeilenstein hochzustufen."
- "오른쪽 상단 모서리의 <strong>승격</strong> 단추를 눌러 그룹 마일스톤으로 보낼 수 있습니다."
- "Cliquez sur le bouton <strong>Promouvoir</strong> en haut à droite pour le promouvoir en tant que jalon de groupe."
- "Haga clic sobre el botón <strong>Promocionar</strong> , situado en la esquina superior derecha para promocionarlo a un hito de grupo."
"Click any <strong>project name</strong> in the project list below to navigate to the project milestone.":
plural_id:
translations:
- "在專案列表點擊任何<strong>專案名稱</strong>,將轉跳到專案的里程碑。"
- "Clique em qualquer <strong>nome de projeto</strong> na lista a seguir para navegar para o marco do projeto."
- "プロジェクトリストで<strong>プロジェクト名</strong>をクリックすると、プロジェクトのマイルストーンに移動します。"
- "Clica em qualquer <strong>nome de projeto</strong> na lista a seguir para navegar para o objetivo do projeto."
- "Выберите из списка любой <strong>проект</strong>, чтобы перейти к этапу проекта."
- "单击下面项目列表中的任何 <strong>项目名称</strong> 跳转到项目里程碑。"
- "Клікніть по будь-якому <strong>імені проекту</strong> зі списку нижче для того, щоб перейти до етапу проекту."
- "Klicke auf einen beliebigen <strong>-Projektnamen</strong> in der folgenden Projektliste, um zum Projektmeilenstein zu navigieren."
- "아래 프로젝트 목록에서 <strong>프로젝트 이름</strong>을 눌러 프로젝트 마일스톤을 봅니다."
- "Cliquez sur nimporte quel <strong>nom de projet</strong> dans la liste des projets cidessous pour naviguer jusquau jalon du projet."
- "Haga clic en cualquier <strong>nombre de proyecto</strong> en la lista de proyectos que se muestra a continuación para navegar hasta el hito de proyecto correspondiente."

View File

@ -159,7 +159,8 @@ module Gitlab
usage_counters,
user_preferences_usage,
ingress_modsecurity_usage,
container_expiration_policies_usage
container_expiration_policies_usage,
service_desk_counts
).tap do |data|
data[:snippets] = data[:personal_snippets] + data[:project_snippets]
end
@ -527,7 +528,9 @@ module Gitlab
issues: distinct_count(::Issue.where(time_period), :author_id),
notes: distinct_count(::Note.where(time_period), :author_id),
projects: distinct_count(::Project.where(time_period), :creator_id),
todos: distinct_count(::Todo.where(time_period), :author_id)
todos: distinct_count(::Todo.where(time_period), :author_id),
service_desk_enabled_projects: distinct_count_service_desk_enabled_projects(time_period),
service_desk_issues: count(::Issue.service_desk.where(time_period))
}
end
# rubocop: enable CodeReuse/ActiveRecord
@ -615,6 +618,30 @@ module Gitlab
private
def distinct_count_service_desk_enabled_projects(time_period)
project_creator_id_start = user_minimum_id
project_creator_id_finish = user_maximum_id
distinct_count(::Project.service_desk_enabled.where(time_period), :creator_id, start: project_creator_id_start, finish: project_creator_id_finish) # rubocop: disable CodeReuse/ActiveRecord
end
# rubocop: disable CodeReuse/ActiveRecord
def service_desk_counts
projects_with_service_desk = ::Project.where(service_desk_enabled: true)
{
service_desk_enabled_projects: count(projects_with_service_desk),
service_desk_issues: count(
::Issue.where(
project: projects_with_service_desk,
author: ::User.support_bot,
confidential: true
)
)
}
end
# rubocop: enable CodeReuse/ActiveRecord
def unique_visit_service
strong_memoize(:unique_visit_service) do
::Gitlab::Analytics::UniqueVisits.new

View File

@ -4875,15 +4875,9 @@ msgstr ""
msgid "Clears weight."
msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
msgid "Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone."
msgstr ""
msgid "Click the <strong>Select none</strong> button on the right, since we only need \"Google Code Project Hosting\"."
msgstr ""
@ -10702,9 +10696,6 @@ msgstr ""
msgid "Generate new token"
msgstr ""
msgid "GenericReports|Report"
msgstr ""
msgid "Geo"
msgstr ""
@ -18982,9 +18973,6 @@ msgstr ""
msgid "Promote issue to an epic"
msgstr ""
msgid "Promote these project milestones into a group milestone."
msgstr ""
msgid "Promote to group label"
msgstr ""
@ -19901,9 +19889,6 @@ msgstr ""
msgid "Reporting"
msgstr ""
msgid "Reports"
msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
@ -24029,9 +24014,6 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
msgid "There was an error while fetching configuration data."
msgstr ""
msgid "There was an error while fetching value stream analytics data."
msgstr ""
@ -24404,9 +24386,6 @@ msgstr ""
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
msgid "This page will be removed in a future release."
msgstr ""
msgid "This pipeline does not use the %{codeStart}needs%{codeEnd} keyword and can't be represented as a directed acyclic graph."
msgstr ""
@ -25824,9 +25803,6 @@ msgstr ""
msgid "Use custom color #FF0000"
msgstr ""
msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr ""
msgid "Use hashed storage"
msgstr ""

View File

@ -19,6 +19,7 @@ RSpec.describe 'Database schema' do
approver_groups: %w[target_id],
approvers: %w[target_id user_id],
audit_events: %w[author_id entity_id],
audit_events_part_5fc467ac26: %w[author_id entity_id],
award_emoji: %w[awardable_id user_id],
aws_roles: %w[role_external_id],
boards: %w[milestone_id],

View File

@ -71,6 +71,60 @@ RSpec.describe 'Database config initializer' do
end
end
context "with new pool size logic" do
let(:max_threads) { 8 }
before do
stub_env('DB_USE_NEW_POOL_SIZE_LOGIC', '1')
allow(Gitlab::Runtime).to receive(:max_threads).and_return(max_threads)
end
context "and no existing pool size is set" do
before do
stub_database_config(pool_size: nil)
end
it "sets it based on the max number of worker threads" do
expect { subject }.to change { Gitlab::Database.config['pool'] }.from(nil).to(18)
end
end
context "and the existing pool size is smaller than the max number of worker threads" do
before do
stub_database_config(pool_size: 1)
end
it "sets it based on the max number of worker threads" do
expect { subject }.to change { Gitlab::Database.config['pool'] }.from(1).to(18)
end
end
context "and the existing pool size is larger than the max number of worker threads" do
before do
stub_database_config(pool_size: 100)
end
it "sets it based on the max number of worker threads" do
expect { subject }.to change { Gitlab::Database.config['pool'] }.from(100).to(18)
end
end
context "when specifying headroom through an ENV variable" do
let(:headroom) { 15 }
before do
stub_database_config(pool_size: 1)
stub_env("DB_POOL_HEADROOM", headroom)
end
it "adds headroom on top of the calculated size" do
expect { subject }.to change { Gitlab::Database.config['pool'] }
.from(1)
.to(max_threads + headroom)
end
end
end
def stub_database_config(pool_size:)
config = {
'adapter' => 'postgresql',

View File

@ -1,61 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::ArchiveLegacyTraces do
include TraceHelpers
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:builds) { table(:ci_builds) }
let(:job_artifacts) { table(:ci_job_artifacts) }
before do
namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1')
projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 123)
@build = builds.create!(id: 1, project_id: 123, status: 'success', type: 'Ci::Build')
end
context 'when trace file exsits at the right place' do
before do
create_legacy_trace(@build, 'trace in file')
end
it 'correctly archive legacy traces' do
expect(job_artifacts.count).to eq(0)
expect(File.exist?(legacy_trace_path(@build))).to be_truthy
described_class.new.perform(1, 1)
expect(job_artifacts.count).to eq(1)
expect(File.exist?(legacy_trace_path(@build))).to be_falsy
expect(File.read(archived_trace_path(job_artifacts.first))).to eq('trace in file')
end
end
context 'when trace file does not exsits at the right place' do
it 'does not raise errors nor create job artifact' do
expect { described_class.new.perform(1, 1) }.not_to raise_error
expect(job_artifacts.count).to eq(0)
end
end
context 'when trace data exsits in database' do
before do
create_legacy_trace_in_db(@build, 'trace in db')
end
it 'correctly archive legacy traces' do
expect(job_artifacts.count).to eq(0)
expect(@build.read_attribute(:trace)).not_to be_empty
described_class.new.perform(1, 1)
@build.reload
expect(job_artifacts.count).to eq(1)
expect(@build.read_attribute(:trace)).to be_nil
expect(File.read(archived_trace_path(job_artifacts.first))).to eq('trace in db')
end
end
end

View File

@ -1,7 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillHashedProjectRepositories do
it_behaves_like 'backfill migration for project repositories', :hashed
end

View File

@ -1,88 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillProjectFullpathInRepoConfig do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:group) { namespaces.create!(name: 'foo', path: 'foo') }
let(:subgroup) { namespaces.create!(name: 'bar', path: 'bar', parent_id: group.id) }
describe described_class::Storage::Hashed do
let(:project) { double(id: 555) }
subject(:project_storage) { described_class.new(project) }
it 'has the correct disk_path' do
expect(project_storage.disk_path).to eq('@hashed/91/a7/91a73fd806ab2c005c13b4dc19130a884e909dea3f72d46e30266fe1a1f588d8')
end
end
describe described_class::Storage::LegacyProject do
let(:project) { double(full_path: 'this/is/the/full/path') }
subject(:project_storage) { described_class.new(project) }
it 'has the correct disk_path' do
expect(project_storage.disk_path).to eq('this/is/the/full/path')
end
end
describe described_class::Project do
let(:project_record) { projects.create!(namespace_id: subgroup.id, name: 'baz', path: 'baz') }
subject(:project) { described_class.find(project_record.id) }
describe '#full_path' do
it 'returns path containing all parent namespaces' do
expect(project.full_path).to eq('foo/bar/baz')
end
it 'raises OrphanedNamespaceError when any parent namespace does not exist' do
subgroup.update_attribute(:parent_id, non_existing_record_id)
expect { project.full_path }.to raise_error(Gitlab::BackgroundMigration::BackfillProjectFullpathInRepoConfig::OrphanedNamespaceError)
end
end
end
describe described_class::Up do
describe '#perform' do
subject(:migrate) { described_class.new.perform(projects.minimum(:id), projects.maximum(:id)) }
it 'asks the gitaly client to set config' do
projects.create!(namespace_id: subgroup.id, name: 'baz', path: 'baz')
projects.create!(namespace_id: subgroup.id, name: 'buzz', path: 'buzz', storage_version: 1)
expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service|
allow(repository_service).to receive(:cleanup)
expect(repository_service).to receive(:set_config).with('gitlab.fullpath' => 'foo/bar/baz')
end
expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service|
allow(repository_service).to receive(:cleanup)
expect(repository_service).to receive(:set_config).with('gitlab.fullpath' => 'foo/bar/buzz')
end
migrate
end
end
end
describe described_class::Down do
describe '#perform' do
subject(:migrate) { described_class.new.perform(projects.minimum(:id), projects.maximum(:id)) }
it 'asks the gitaly client to set config' do
projects.create!(namespace_id: subgroup.id, name: 'baz', path: 'baz')
expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service|
allow(repository_service).to receive(:cleanup)
expect(repository_service).to receive(:delete_config).with(['gitlab.fullpath'])
end
migrate
end
end
end
end

View File

@ -1,111 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::FixCrossProjectLabelLinks do
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:issues_table) { table(:issues) }
let(:merge_requests_table) { table(:merge_requests) }
let(:labels_table) { table(:labels) }
let(:label_links_table) { table(:label_links) }
let!(:group1) { namespaces_table.create(id: 10, type: 'Group', name: 'group1', path: 'group1') }
let!(:group2) { namespaces_table.create(id: 20, type: 'Group', name: 'group2', path: 'group2') }
let!(:project1) { projects_table.create(id: 1, name: 'project1', path: 'group1/project1', namespace_id: 10) }
let!(:project2) { projects_table.create(id: 3, name: 'project2', path: 'group1/project2', namespace_id: 20) }
let!(:label1) { labels_table.create(id: 1, title: 'bug', color: 'red', group_id: 10, type: 'GroupLabel') }
let!(:label2) { labels_table.create(id: 2, title: 'bug', color: 'red', group_id: 20, type: 'GroupLabel') }
def create_merge_request(id, project_id)
merge_requests_table.create(id: id,
target_project_id: project_id,
target_branch: 'master',
source_project_id: project_id,
source_branch: 'mr name',
title: "mr name#{id}")
end
def create_issue(id, project_id)
issues_table.create(id: id, title: "issue#{id}", project_id: project_id)
end
def create_resource(target_type, id, project_id)
target_type == 'Issue' ? create_issue(id, project_id) : create_merge_request(id, project_id)
end
shared_examples_for 'resource with cross-project labels' do
it 'updates only cross-project label links which exist in the local project or group' do
create_resource(target_type, 1, 1)
create_resource(target_type, 2, 3)
labels_table.create(id: 3, title: 'bug', color: 'red', project_id: 3, type: 'ProjectLabel')
link = label_links_table.create(label_id: 2, target_type: target_type, target_id: 1)
link2 = label_links_table.create(label_id: 3, target_type: target_type, target_id: 2)
subject.perform(1, 100)
expect(link.reload.label_id).to eq(1)
expect(link2.reload.label_id).to eq(3)
end
it 'ignores cross-project label links if label color is different' do
labels_table.create(id: 3, title: 'bug', color: 'green', group_id: 20, type: 'GroupLabel')
create_resource(target_type, 1, 1)
link = label_links_table.create(label_id: 3, target_type: target_type, target_id: 1)
subject.perform(1, 100)
expect(link.reload.label_id).to eq(3)
end
it 'ignores cross-project label links if label name is different' do
labels_table.create(id: 3, title: 'bug1', color: 'red', group_id: 20, type: 'GroupLabel')
create_resource(target_type, 1, 1)
link = label_links_table.create(label_id: 3, target_type: target_type, target_id: 1)
subject.perform(1, 100)
expect(link.reload.label_id).to eq(3)
end
context 'with nested group' do
before do
namespaces_table.create(id: 11, type: 'Group', name: 'subgroup1', path: 'group1/subgroup1', parent_id: 10)
projects_table.create(id: 2, name: 'subproject1', path: 'group1/subgroup1/subproject1', namespace_id: 11)
create_resource(target_type, 1, 2)
end
it 'ignores label links referencing ancestor group labels' do
labels_table.create(id: 4, title: 'bug', color: 'red', project_id: 2, type: 'ProjectLabel')
label_links_table.create(label_id: 4, target_type: target_type, target_id: 1)
link = label_links_table.create(label_id: 1, target_type: target_type, target_id: 1)
subject.perform(1, 100)
expect(link.reload.label_id).to eq(1)
end
it 'checks also issues and MRs in subgroups' do
link = label_links_table.create(label_id: 2, target_type: target_type, target_id: 1)
subject.perform(1, 100)
expect(link.reload.label_id).to eq(1)
end
end
end
context 'resource is Issue' do
it_behaves_like 'resource with cross-project labels' do
let(:target_type) { 'Issue' }
end
end
context 'resource is Merge Request' do
it_behaves_like 'resource with cross-project labels' do
let(:target_type) { 'MergeRequest' }
end
end
end

View File

@ -1,84 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::MigrateBuildStage do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:pipelines) { table(:ci_pipelines) }
let(:stages) { table(:ci_stages) }
let(:jobs) { table(:ci_builds) }
let(:statuses) do
{
created: 0,
pending: 1,
running: 2,
success: 3,
failed: 4,
canceled: 5,
skipped: 6,
manual: 7
}
end
before do
namespace = namespaces.create!(name: 'gitlab-org', path: 'gitlab-org')
projects.create!(id: 123, name: 'gitlab', path: 'gitlab-ce', namespace_id: namespace.id)
pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a')
jobs.create!(id: 1, commit_id: 1, project_id: 123,
stage_idx: 2, stage: 'build', status: :success)
jobs.create!(id: 2, commit_id: 1, project_id: 123,
stage_idx: 2, stage: 'build', status: :success)
jobs.create!(id: 3, commit_id: 1, project_id: 123,
stage_idx: 1, stage: 'test', status: :failed)
jobs.create!(id: 4, commit_id: 1, project_id: 123,
stage_idx: 1, stage: 'test', status: :success)
jobs.create!(id: 5, commit_id: 1, project_id: 123,
stage_idx: 3, stage: 'deploy', status: :pending)
jobs.create!(id: 6, commit_id: 1, project_id: 123,
stage_idx: 3, stage: nil, status: :pending)
end
it 'correctly migrates builds stages' do
expect(stages.count).to be_zero
described_class.new.perform(1, 6)
expect(stages.count).to eq 3
expect(stages.all.pluck(:name)).to match_array %w[test build deploy]
expect(jobs.where(stage_id: nil)).to be_one
expect(jobs.find_by(stage_id: nil).id).to eq 6
expect(stages.all.pluck(:status)).to match_array [statuses[:success],
statuses[:failed],
statuses[:pending]]
end
it 'recovers from unique constraint violation only twice', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/28128' do
allow(described_class::Migratable::Stage)
.to receive(:find_by).and_return(nil)
expect(described_class::Migratable::Stage)
.to receive(:find_by).exactly(3).times
expect { described_class.new.perform(1, 6) }
.to raise_error ActiveRecord::RecordNotUnique
end
context 'when invalid class can be loaded due to single table inheritance' do
let(:commit_status) do
jobs.create!(id: 7, commit_id: 1, project_id: 123, stage_idx: 4,
stage: 'post-deploy', status: :failed)
end
before do
commit_status.update_column(:type, 'SomeClass')
end
it 'does ignore single table inheritance type' do
expect { described_class.new.perform(1, 7) }.not_to raise_error
expect(jobs.find(7)).to have_attributes(stage_id: (a_value > 0))
end
end
end

View File

@ -1,35 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::MigrateStageIndex do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:pipelines) { table(:ci_pipelines) }
let(:stages) { table(:ci_stages) }
let(:jobs) { table(:ci_builds) }
let(:namespace) { namespaces.create(name: 'gitlab-org', path: 'gitlab-org') }
let(:project) { projects.create!(namespace_id: namespace.id, name: 'gitlab', path: 'gitlab') }
let(:pipeline) { pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a') }
let(:stage1) { stages.create(project_id: project.id, pipeline_id: pipeline.id, name: 'build') }
let(:stage2) { stages.create(project_id: project.id, pipeline_id: pipeline.id, name: 'test') }
before do
jobs.create!(commit_id: pipeline.id, project_id: project.id,
stage_idx: 2, stage_id: stage1.id)
jobs.create!(commit_id: pipeline.id, project_id: project.id,
stage_idx: 2, stage_id: stage1.id)
jobs.create!(commit_id: pipeline.id, project_id: project.id,
stage_idx: 10, stage_id: stage1.id)
jobs.create!(commit_id: pipeline.id, project_id: project.id,
stage_idx: 3, stage_id: stage2.id)
end
it 'correctly migrates stages indices' do
expect(stages.all.pluck(:position)).to all(be_nil)
described_class.new.perform(stage1.id, stage2.id)
expect(stages.all.order(:id).pluck(:position)).to eq [2, 3]
end
end

View File

@ -1,94 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable do
include MigrationHelpers::ClusterHelpers
let(:migration) { described_class.new }
let(:clusters_table) { table(:clusters) }
let(:cluster_projects_table) { table(:cluster_projects) }
let(:cluster_kubernetes_namespaces_table) { table(:clusters_kubernetes_namespaces) }
let(:projects_table) { table(:projects) }
let(:namespaces_table) { table(:namespaces) }
let(:provider_gcp_table) { table(:cluster_providers_gcp) }
let(:platform_kubernetes_table) { table(:cluster_platforms_kubernetes) }
before do
create_cluster_project_list(10)
end
shared_examples 'consistent kubernetes namespace attributes' do
it 'populates namespace and service account information' do
migration.perform
clusters_with_namespace.each do |cluster|
cluster_project = cluster_projects_table.find_by(cluster_id: cluster.id)
project = projects_table.find(cluster_project.project_id)
kubernetes_namespace = cluster_kubernetes_namespaces_table.find_by(cluster_id: cluster.id)
namespace = "#{project.path}-#{project.id}"
expect(kubernetes_namespace).to be_present
expect(kubernetes_namespace.cluster_project_id).to eq(cluster_project.id)
expect(kubernetes_namespace.project_id).to eq(cluster_project.project_id)
expect(kubernetes_namespace.cluster_id).to eq(cluster_project.cluster_id)
expect(kubernetes_namespace.namespace).to eq(namespace)
expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account")
end
end
end
context 'when no Clusters::Project has a Clusters::KubernetesNamespace' do
let(:cluster_projects) { cluster_projects_table.all }
it 'creates a Clusters::KubernetesNamespace per Clusters::Project' do
expect do
migration.perform
end.to change(Clusters::KubernetesNamespace, :count).by(cluster_projects_table.count)
end
it_behaves_like 'consistent kubernetes namespace attributes' do
let(:clusters_with_namespace) { clusters_table.all }
end
end
context 'when every Clusters::Project has Clusters::KubernetesNamespace' do
before do
create_kubernetes_namespace(clusters_table.all)
end
it 'does not create any Clusters::KubernetesNamespace' do
expect do
migration.perform
end.not_to change(Clusters::KubernetesNamespace, :count)
end
end
context 'when only some Clusters::Project have Clusters::KubernetesNamespace related' do
let(:with_kubernetes_namespace) { clusters_table.first(6) }
let(:with_no_kubernetes_namespace) { clusters_table.last(4) }
before do
create_kubernetes_namespace(with_kubernetes_namespace)
end
it 'creates limited number of Clusters::KubernetesNamespace' do
expect do
migration.perform
end.to change(Clusters::KubernetesNamespace, :count).by(with_no_kubernetes_namespace.count)
end
it 'does not modify clusters with Clusters::KubernetesNamespace' do
migration.perform
with_kubernetes_namespace.each do |cluster|
kubernetes_namespace = cluster_kubernetes_namespaces_table.where(cluster_id: cluster.id)
expect(kubernetes_namespace.count).to eq(1)
end
end
it_behaves_like 'consistent kubernetes namespace attributes' do
let(:clusters_with_namespace) { with_no_kubernetes_namespace }
end
end
end

View File

@ -1,126 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::RemoveRestrictedTodos do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:users) { table(:users) }
let(:todos) { table(:todos) }
let(:issues) { table(:issues) }
let(:assignees) { table(:issue_assignees) }
let(:project_authorizations) { table(:project_authorizations) }
let(:project_features) { table(:project_features) }
let(:todo_params) { { author_id: 1, target_type: 'Issue', action: 1, state: :pending } }
before do
users.create(id: 1, email: 'user@example.com', projects_limit: 10)
users.create(id: 2, email: 'reporter@example.com', projects_limit: 10)
users.create(id: 3, email: 'guest@example.com', projects_limit: 10)
namespace = namespaces.create(name: 'gitlab-org', path: 'gitlab-org')
projects.create!(id: 1, name: 'project-1', path: 'project-1', visibility_level: 0, namespace_id: namespace.id)
projects.create!(id: 2, name: 'project-2', path: 'project-2', visibility_level: 0, namespace_id: namespace.id)
issues.create(id: 1, project_id: 1)
issues.create(id: 2, project_id: 2)
project_authorizations.create(user_id: 2, project_id: 2, access_level: 20) # reporter
project_authorizations.create(user_id: 3, project_id: 2, access_level: 10) # guest
todos.create(todo_params.merge(user_id: 1, project_id: 1, target_id: 1)) # out of project ids range
todos.create(todo_params.merge(user_id: 1, project_id: 2, target_id: 2)) # non member
todos.create(todo_params.merge(user_id: 2, project_id: 2, target_id: 2)) # reporter
todos.create(todo_params.merge(user_id: 3, project_id: 2, target_id: 2)) # guest
end
subject { described_class.new.perform(2, 5) }
context 'when a project is private' do
it 'removes todos of users without project access' do
expect { subject }.to change { Todo.count }.from(4).to(3)
end
context 'with a confidential issue' do
it 'removes todos of users without project access and guests for confidential issues' do
issues.create(id: 3, project_id: 2, confidential: true)
issues.create(id: 4, project_id: 1, confidential: true) # not in the batch
todos.create(todo_params.merge(user_id: 3, project_id: 2, target_id: 3))
todos.create(todo_params.merge(user_id: 2, project_id: 2, target_id: 3))
todos.create(todo_params.merge(user_id: 1, project_id: 1, target_id: 4))
expect { subject }.to change { Todo.count }.from(7).to(5)
end
end
end
context 'when a project is public' do
before do
projects.find(2).update_attribute(:visibility_level, 20)
end
context 'when all features have the same visibility as the project, no confidential issues' do
it 'does not remove any todos' do
expect { subject }.not_to change { Todo.count }
end
end
context 'with confidential issues' do
before do
users.create(id: 4, email: 'author@example.com', projects_limit: 10)
users.create(id: 5, email: 'assignee@example.com', projects_limit: 10)
issues.create(id: 3, project_id: 2, confidential: true, author_id: 4)
assignees.create(user_id: 5, issue_id: 3)
todos.create(todo_params.merge(user_id: 1, project_id: 2, target_id: 3)) # to be deleted
todos.create(todo_params.merge(user_id: 2, project_id: 2, target_id: 3)) # authorized user
todos.create(todo_params.merge(user_id: 3, project_id: 2, target_id: 3)) # to be deleted guest
todos.create(todo_params.merge(user_id: 4, project_id: 2, target_id: 3)) # conf issue author
todos.create(todo_params.merge(user_id: 5, project_id: 2, target_id: 3)) # conf issue assignee
end
it 'removes confidential issue todos for non authorized users' do
expect { subject }.to change { Todo.count }.from(9).to(7)
end
end
context 'features visibility restrictions' do
before do
todo_params.merge!(project_id: 2, user_id: 1, target_id: 3)
todos.create(todo_params.merge(user_id: 1, target_id: 3, target_type: 'MergeRequest'))
todos.create(todo_params.merge(user_id: 1, target_id: 3, target_type: 'Commit'))
end
context 'when issues are restricted to project members' do
before do
project_features.create(issues_access_level: 10, pages_access_level: 10, project_id: 2)
end
it 'removes non members issue todos' do
expect { subject }.to change { Todo.count }.from(6).to(5)
end
end
context 'when merge requests are restricted to project members' do
before do
project_features.create(merge_requests_access_level: 10, pages_access_level: 10, project_id: 2)
end
it 'removes non members issue todos' do
expect { subject }.to change { Todo.count }.from(6).to(5)
end
end
context 'when repository and merge requests are restricted to project members' do
before do
project_features.create(repository_access_level: 10, merge_requests_access_level: 10, pages_access_level: 10, project_id: 2)
end
it 'removes non members commit and merge requests todos' do
expect { subject }.to change { Todo.count }.from(6).to(4)
end
end
end
end
end

View File

@ -1,33 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnServices do
let(:services) { table(:services) }
describe '#perform' do
it 'migrates services where note_events is true' do
service = services.create(confidential_note_events: nil, note_events: true)
subject.perform(service.id, service.id)
expect(service.reload.confidential_note_events).to eq(true)
end
it 'ignores services where note_events is false' do
service = services.create(confidential_note_events: nil, note_events: false)
subject.perform(service.id, service.id)
expect(service.reload.confidential_note_events).to eq(nil)
end
it 'ignores services where confidential_note_events has already been set' do
service = services.create(confidential_note_events: false, note_events: true)
subject.perform(service.id, service.id)
expect(service.reload.confidential_note_events).to eq(false)
end
end
end

View File

@ -1,33 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnWebhooks do
let(:web_hooks) { table(:web_hooks) }
describe '#perform' do
it 'migrates hooks where note_events is true' do
hook = web_hooks.create(confidential_note_events: nil, note_events: true)
subject.perform(hook.id, hook.id)
expect(hook.reload.confidential_note_events).to eq(true)
end
it 'ignores hooks where note_events is false' do
hook = web_hooks.create(confidential_note_events: nil, note_events: false)
subject.perform(hook.id, hook.id)
expect(hook.reload.confidential_note_events).to eq(nil)
end
it 'ignores hooks where confidential_note_events has already been set' do
hook = web_hooks.create(confidential_note_events: false, note_events: true)
subject.perform(hook.id, hook.id)
expect(hook.reload.confidential_note_events).to eq(false)
end
end
end

View File

@ -75,6 +75,14 @@ RSpec.describe Gitlab::BackgroundMigration::UserMentions::CreateResourceUserMent
let(:resource) { merge_request }
it_behaves_like 'resource mentions migration', MigrateMergeRequestMentionsToDb, MergeRequest
context 'when FF disabled' do
before do
stub_feature_flags(migrate_user_mentions: false)
end
it_behaves_like 'resource migration not run', MigrateMergeRequestMentionsToDb, MergeRequest
end
end
context 'migrate commit mentions' do
@ -96,6 +104,14 @@ RSpec.describe Gitlab::BackgroundMigration::UserMentions::CreateResourceUserMent
let(:resource) { commit }
it_behaves_like 'resource notes mentions migration', MigrateCommitNotesMentionsToDb, Commit
context 'when FF disabled' do
before do
stub_feature_flags(migrate_user_mentions: false)
end
it_behaves_like 'resource notes migration not run', MigrateCommitNotesMentionsToDb, Commit
end
end
end

View File

@ -98,21 +98,21 @@ RSpec.describe Gitlab::Danger::Helper do
it 'delegates to CHANGELOG-EE.md existence if CI_PROJECT_NAME is set to something else' do
stub_env('CI_PROJECT_NAME', 'something else')
expect(Dir).to receive(:exist?).with('../../ee') { true }
expect(Dir).to receive(:exist?).with(File.expand_path('../../../../ee', __dir__)) { true }
is_expected.to be_truthy
end
it 'returns true if ee exists' do
stub_env('CI_PROJECT_NAME', nil)
expect(Dir).to receive(:exist?).with('../../ee') { true }
expect(Dir).to receive(:exist?).with(File.expand_path('../../../../ee', __dir__)) { true }
is_expected.to be_truthy
end
it "returns false if ee doesn't exist" do
stub_env('CI_PROJECT_NAME', nil)
expect(Dir).to receive(:exist?).with('../../ee') { false }
expect(Dir).to receive(:exist?).with(File.expand_path('../../../../ee', __dir__)) { false }
is_expected.to be_falsy
end

View File

@ -1,6 +1,5 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'webmock/rspec'
require 'timecop'
@ -11,102 +10,95 @@ RSpec.describe Gitlab::Danger::Roulette do
Timecop.freeze(Time.utc(2020, 06, 22, 10)) { example.run }
end
let(:backend_available) { true }
let(:backend_tz_offset_hours) { 2.0 }
let(:backend_maintainer) do
{
username: 'backend-maintainer',
name: 'Backend maintainer',
role: 'Backend engineer',
projects: { 'gitlab' => 'maintainer backend' },
available: true,
tz_offset_hours: 2.0
}
Gitlab::Danger::Teammate.new(
'username' => 'backend-maintainer',
'name' => 'Backend maintainer',
'role' => 'Backend engineer',
'projects' => { 'gitlab' => 'maintainer backend' },
'available' => backend_available,
'tz_offset_hours' => backend_tz_offset_hours
)
end
let(:frontend_reviewer) do
{
username: 'frontend-reviewer',
name: 'Frontend reviewer',
role: 'Frontend engineer',
projects: { 'gitlab' => 'reviewer frontend' },
available: true,
tz_offset_hours: 2.0
}
Gitlab::Danger::Teammate.new(
'username' => 'frontend-reviewer',
'name' => 'Frontend reviewer',
'role' => 'Frontend engineer',
'projects' => { 'gitlab' => 'reviewer frontend' },
'available' => true,
'tz_offset_hours' => 2.0
)
end
let(:frontend_maintainer) do
{
username: 'frontend-maintainer',
name: 'Frontend maintainer',
role: 'Frontend engineer',
projects: { 'gitlab' => "maintainer frontend" },
available: true,
tz_offset_hours: 2.0
}
Gitlab::Danger::Teammate.new(
'username' => 'frontend-maintainer',
'name' => 'Frontend maintainer',
'role' => 'Frontend engineer',
'projects' => { 'gitlab' => "maintainer frontend" },
'available' => true,
'tz_offset_hours' => 2.0
)
end
let(:software_engineer_in_test) do
{
username: 'software-engineer-in-test',
name: 'Software Engineer in Test',
role: 'Software Engineer in Test, Create:Source Code',
projects: {
'gitlab' => 'reviewer qa',
'gitlab-qa' => 'maintainer'
},
available: true,
tz_offset_hours: 2.0
}
Gitlab::Danger::Teammate.new(
'username' => 'software-engineer-in-test',
'name' => 'Software Engineer in Test',
'role' => 'Software Engineer in Test, Create:Source Code',
'projects' => { 'gitlab' => 'reviewer qa', 'gitlab-qa' => 'maintainer' },
'available' => true,
'tz_offset_hours' => 2.0
)
end
let(:engineering_productivity_reviewer) do
{
username: 'eng-prod-reviewer',
name: 'EP engineer',
role: 'Engineering Productivity',
projects: { 'gitlab' => 'reviewer backend' },
available: true,
tz_offset_hours: 2.0
}
Gitlab::Danger::Teammate.new(
'username' => 'eng-prod-reviewer',
'name' => 'EP engineer',
'role' => 'Engineering Productivity',
'projects' => { 'gitlab' => 'reviewer backend' },
'available' => true,
'tz_offset_hours' => 2.0
)
end
let(:teammate_json) do
[
backend_maintainer,
frontend_maintainer,
frontend_reviewer,
software_engineer_in_test,
engineering_productivity_reviewer
backend_maintainer.to_h,
frontend_maintainer.to_h,
frontend_reviewer.to_h,
software_engineer_in_test.to_h,
engineering_productivity_reviewer.to_h
].to_json
end
subject(:roulette) { Object.new.extend(described_class) }
def matching_teammate(person)
satisfy do |teammate|
teammate.username == person[:username] &&
teammate.name == person[:name] &&
teammate.role == person[:role] &&
teammate.projects == person[:projects]
end
end
describe 'Spin#==' do
it 'compares Spin attributes' do
spin1 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, false, false)
spin2 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, false, false)
spin3 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, false, true)
spin4 = described_class::Spin.new(:backend, frontend_reviewer, frontend_maintainer, true, false)
spin5 = described_class::Spin.new(:backend, frontend_reviewer, backend_maintainer, false, false)
spin6 = described_class::Spin.new(:backend, backend_maintainer, frontend_maintainer, false, false)
spin7 = described_class::Spin.new(:frontend, frontend_reviewer, frontend_maintainer, false, false)
def matching_spin(category, reviewer: { username: nil }, maintainer: { username: nil }, optional: nil)
satisfy do |spin|
bool = spin.category == category
bool &&= spin.reviewer&.username == reviewer[:username]
bool &&=
if maintainer
spin.maintainer&.username == maintainer[:username]
else
spin.maintainer.nil?
end
bool && spin.optional_role == optional
expect(spin1).to eq(spin2)
expect(spin1).not_to eq(spin3)
expect(spin1).not_to eq(spin4)
expect(spin1).not_to eq(spin5)
expect(spin1).not_to eq(spin6)
expect(spin1).not_to eq(spin7)
end
end
describe '#spin' do
let!(:project) { 'gitlab' }
let!(:branch_name) { 'a-branch' }
let!(:mr_source_branch) { 'a-branch' }
let!(:mr_labels) { ['backend', 'devops::create'] }
let!(:author) { Gitlab::Danger::Teammate.new('username' => 'filipa') }
let!(:author) { Gitlab::Danger::Teammate.new('username' => 'johndoe') }
let(:timezone_experiment) { false }
let(:spins) do
# Stub the request at the latest time so that we can modify the raw data, e.g. available fields.
@ -114,12 +106,13 @@ RSpec.describe Gitlab::Danger::Roulette do
.stub_request(:get, described_class::ROULETTE_DATA_URL)
.to_return(body: teammate_json)
subject.spin(project, categories, branch_name, timezone_experiment: timezone_experiment)
subject.spin(project, categories, timezone_experiment: timezone_experiment)
end
before do
allow(subject).to receive_message_chain(:gitlab, :mr_author).and_return(author.username)
allow(subject).to receive_message_chain(:gitlab, :mr_labels).and_return(mr_labels)
allow(subject).to receive(:mr_author_username).and_return(author.username)
allow(subject).to receive(:mr_labels).and_return(mr_labels)
allow(subject).to receive(:mr_source_branch).and_return(mr_source_branch)
end
context 'when timezone_experiment == false' do
@ -127,16 +120,16 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:categories) { [:backend] }
it 'assigns backend reviewer and maintainer' do
expect(spins).to contain_exactly(matching_spin(:backend, reviewer: engineering_productivity_reviewer, maintainer: backend_maintainer))
expect(spins[0].reviewer).to eq(engineering_productivity_reviewer)
expect(spins[0].maintainer).to eq(backend_maintainer)
expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, false)])
end
context 'when teammate is not available' do
before do
backend_maintainer[:available] = false
end
let(:backend_available) { false }
it 'assigns backend reviewer and no maintainer' do
expect(spins).to contain_exactly(matching_spin(:backend, reviewer: engineering_productivity_reviewer, maintainer: nil))
expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, nil, false, false)])
end
end
end
@ -145,7 +138,7 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:categories) { [:frontend] }
it 'assigns frontend reviewer and maintainer' do
expect(spins).to contain_exactly(matching_spin(:frontend, reviewer: frontend_reviewer, maintainer: frontend_maintainer))
expect(spins).to eq([described_class::Spin.new(:frontend, frontend_reviewer, frontend_maintainer, false, false)])
end
end
@ -153,7 +146,7 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:categories) { [:qa] }
it 'assigns QA reviewer' do
expect(spins).to contain_exactly(matching_spin(:qa, reviewer: software_engineer_in_test))
expect(spins).to eq([described_class::Spin.new(:qa, software_engineer_in_test, nil, false, false)])
end
end
@ -161,7 +154,7 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:categories) { [:engineering_productivity] }
it 'assigns Engineering Productivity reviewer and fallback to backend maintainer' do
expect(spins).to contain_exactly(matching_spin(:engineering_productivity, reviewer: engineering_productivity_reviewer, maintainer: backend_maintainer))
expect(spins).to eq([described_class::Spin.new(:engineering_productivity, engineering_productivity_reviewer, backend_maintainer, false, false)])
end
end
@ -169,7 +162,7 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:categories) { [:test] }
it 'assigns corresponding SET' do
expect(spins).to contain_exactly(matching_spin(:test, reviewer: software_engineer_in_test))
expect(spins).to eq([described_class::Spin.new(:test, software_engineer_in_test, nil, :maintainer, false)])
end
end
end
@ -181,16 +174,14 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:categories) { [:backend] }
it 'assigns backend reviewer and maintainer' do
expect(spins).to contain_exactly(matching_spin(:backend, reviewer: engineering_productivity_reviewer, maintainer: backend_maintainer))
expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, true)])
end
context 'when teammate is not in a good timezone' do
before do
backend_maintainer[:tz_offset_hours] = 5.0
end
let(:backend_tz_offset_hours) { 5.0 }
it 'assigns backend reviewer and no maintainer' do
expect(spins).to contain_exactly(matching_spin(:backend, reviewer: engineering_productivity_reviewer, maintainer: nil))
expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, nil, false, true)])
end
end
end
@ -203,22 +194,33 @@ RSpec.describe Gitlab::Danger::Roulette do
end
it 'assigns backend reviewer and maintainer' do
expect(spins).to contain_exactly(matching_spin(:backend, reviewer: engineering_productivity_reviewer, maintainer: backend_maintainer))
expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, false)])
end
context 'when teammate is not in a good timezone' do
before do
backend_maintainer[:tz_offset_hours] = 5.0
end
let(:backend_tz_offset_hours) { 5.0 }
it 'assigns backend reviewer and maintainer' do
expect(spins).to contain_exactly(matching_spin(:backend, reviewer: engineering_productivity_reviewer, maintainer: backend_maintainer))
expect(spins).to eq([described_class::Spin.new(:backend, engineering_productivity_reviewer, backend_maintainer, false, false)])
end
end
end
end
end
RSpec::Matchers.define :match_teammates do |expected|
match do |actual|
expected.each do |expected_person|
actual_person_found = actual.find { |actual_person| actual_person.name == expected_person.username }
actual_person_found &&
actual_person_found.name == expected_person.name &&
actual_person_found.role == expected_person.role &&
actual_person_found.projects == expected_person.projects
end
end
end
describe '#team' do
subject(:team) { roulette.team }
@ -254,15 +256,13 @@ RSpec.describe Gitlab::Danger::Roulette do
end
it 'returns an array of teammates' do
expected_teammates = [
matching_teammate(backend_maintainer),
matching_teammate(frontend_reviewer),
matching_teammate(frontend_maintainer),
matching_teammate(software_engineer_in_test),
matching_teammate(engineering_productivity_reviewer)
]
is_expected.to contain_exactly(*expected_teammates)
is_expected.to match_teammates([
backend_maintainer,
frontend_reviewer,
frontend_maintainer,
software_engineer_in_test,
engineering_productivity_reviewer
])
end
it 'memoizes the result' do
@ -281,7 +281,9 @@ RSpec.describe Gitlab::Danger::Roulette do
end
it 'filters team by project_name' do
is_expected.to contain_exactly(matching_teammate(software_engineer_in_test))
is_expected.to match_teammates([
software_engineer_in_test
])
end
end
@ -289,32 +291,32 @@ RSpec.describe Gitlab::Danger::Roulette do
let(:person_tz_offset_hours) { 0.0 }
let(:person1) do
Gitlab::Danger::Teammate.new(
'username' => 'rymai',
'username' => 'user1',
'available' => true,
'tz_offset_hours' => person_tz_offset_hours
)
end
let(:person2) do
Gitlab::Danger::Teammate.new(
'username' => 'godfat',
'username' => 'user2',
'available' => true,
'tz_offset_hours' => person_tz_offset_hours)
end
let(:author) do
Gitlab::Danger::Teammate.new(
'username' => 'filipa',
'username' => 'johndoe',
'available' => true,
'tz_offset_hours' => 0.0)
end
let(:unavailable) do
Gitlab::Danger::Teammate.new(
'username' => 'jacopo-beschi',
'username' => 'janedoe',
'available' => false,
'tz_offset_hours' => 0.0)
end
before do
allow(subject).to receive_message_chain(:gitlab, :mr_author).and_return(author.username)
allow(subject).to receive(:mr_author_username).and_return(author.username)
end
(-4..4).each do |utc_offset|
@ -328,7 +330,7 @@ RSpec.describe Gitlab::Danger::Roulette do
selected = subject.spin_for_person(persons, random: Random.new, timezone_experiment: timezone_experiment)
expect(selected.username).to be_in(persons.map(&:username))
expect(persons.map(&:username)).to include(selected.username)
end
end
end
@ -349,7 +351,7 @@ RSpec.describe Gitlab::Danger::Roulette do
if timezone_experiment
expect(selected).to be_nil
else
expect(selected.username).to be_in(persons.map(&:username))
expect(persons.map(&:username)).to include(selected.username)
end
end
end

View File

@ -1,7 +1,5 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'timecop'
require 'rspec-parameterized'
@ -10,16 +8,16 @@ require 'gitlab/danger/teammate'
RSpec.describe Gitlab::Danger::Teammate do
using RSpec::Parameterized::TableSyntax
subject { described_class.new(options.stringify_keys) }
subject { described_class.new(options) }
let(:tz_offset_hours) { 2.0 }
let(:options) do
{
username: 'luigi',
projects: projects,
role: role,
markdown_name: '[Luigi](https://gitlab.com/luigi) (`@luigi`)',
tz_offset_hours: tz_offset_hours
'username' => 'luigi',
'projects' => projects,
'role' => role,
'markdown_name' => '[Luigi](https://gitlab.com/luigi) (`@luigi`)',
'tz_offset_hours' => tz_offset_hours
}
end
let(:capabilities) { ['reviewer backend'] }
@ -28,6 +26,26 @@ RSpec.describe Gitlab::Danger::Teammate do
let(:labels) { [] }
let(:project) { double }
describe '#==' do
it 'compares Teammate username' do
joe1 = described_class.new('username' => 'joe', 'projects' => projects)
joe2 = described_class.new('username' => 'joe', 'projects' => [])
jane1 = described_class.new('username' => 'jane', 'projects' => projects)
jane2 = described_class.new('username' => 'jane', 'projects' => [])
expect(joe1).to eq(joe2)
expect(jane1).to eq(jane2)
expect(jane1).not_to eq(nil)
expect(described_class.new('username' => nil)).not_to eq(nil)
end
end
describe '#to_h' do
it 'returns the given options' do
expect(subject.to_h).to eq(options)
end
end
context 'when having multiple capabilities' do
let(:capabilities) { ['reviewer backend', 'maintainer frontend', 'trainee_maintainer qa'] }
@ -153,21 +171,21 @@ RSpec.describe Gitlab::Danger::Teammate do
describe '#markdown_name' do
context 'when timezone_experiment == false' do
it 'returns markdown name as-is' do
expect(subject.markdown_name).to eq(options[:markdown_name])
expect(subject.markdown_name(timezone_experiment: false)).to eq(options[:markdown_name])
expect(subject.markdown_name).to eq(options['markdown_name'])
expect(subject.markdown_name(timezone_experiment: false)).to eq(options['markdown_name'])
end
end
context 'when timezone_experiment == true' do
it 'returns markdown name with timezone info' do
expect(subject.markdown_name(timezone_experiment: true)).to eq("#{options[:markdown_name]} (UTC+2)")
expect(subject.markdown_name(timezone_experiment: true)).to eq("#{options['markdown_name']} (UTC+2)")
end
context 'when offset is 1.5' do
let(:tz_offset_hours) { 1.5 }
it 'returns markdown name with timezone info, not truncated' do
expect(subject.markdown_name(timezone_experiment: true)).to eq("#{options[:markdown_name]} (UTC+1.5)")
expect(subject.markdown_name(timezone_experiment: true)).to eq("#{options['markdown_name']} (UTC+1.5)")
end
end
@ -185,12 +203,12 @@ RSpec.describe Gitlab::Danger::Teammate do
with_them do
it 'returns markdown name with timezone info' do
author = described_class.new(options.merge(username: 'mario', tz_offset_hours: author_offset).stringify_keys)
author = described_class.new(options.merge('username' => 'mario', 'tz_offset_hours' => author_offset))
floored_offset_hours = subject.__send__(:floored_offset_hours)
utc_offset = floored_offset_hours >= 0 ? "+#{floored_offset_hours}" : floored_offset_hours
expect(subject.markdown_name(timezone_experiment: true, author: author)).to eq("#{options[:markdown_name]} (UTC#{utc_offset}, #{diff_text})")
expect(subject.markdown_name(timezone_experiment: true, author: author)).to eq("#{options['markdown_name']} (UTC#{utc_offset}, #{diff_text})")
end
end
end

View File

@ -155,13 +155,13 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(described_class.uncached_data[:usage_activity_by_stage][:manage]).to include(
events: 2,
groups: 2,
users_created: Gitlab.ee? ? 6 : 5,
users_created: 6,
omniauth_providers: ['google_oauth2']
)
expect(described_class.uncached_data[:usage_activity_by_stage_monthly][:manage]).to include(
events: 1,
groups: 1,
users_created: Gitlab.ee? ? 4 : 3,
users_created: 4,
omniauth_providers: ['google_oauth2']
)
end
@ -203,21 +203,26 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
user = create(:user)
project = create(:project, creator: user)
issue = create(:issue, project: project, author: user)
create(:issue, project: project, author: User.support_bot)
create(:note, project: project, noteable: issue, author: user)
create(:todo, project: project, target: issue, author: user)
end
expect(described_class.uncached_data[:usage_activity_by_stage][:plan]).to include(
issues: 2,
issues: 3,
notes: 2,
projects: 2,
todos: 2
todos: 2,
service_desk_enabled_projects: 2,
service_desk_issues: 2
)
expect(described_class.uncached_data[:usage_activity_by_stage_monthly][:plan]).to include(
issues: 1,
issues: 2,
notes: 1,
projects: 1,
todos: 1
todos: 1,
service_desk_enabled_projects: 1,
service_desk_issues: 1
)
end
end
@ -974,4 +979,17 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
})
end
end
describe '.service_desk_counts' do
subject { described_class.send(:service_desk_counts) }
let(:project) { create(:project, :service_desk_enabled) }
it 'gathers Service Desk data' do
create_list(:issue, 2, :confidential, author: User.support_bot, project: project)
expect(subject).to eq(service_desk_enabled_projects: 1,
service_desk_issues: 2)
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200601120434_migrate_all_merge_request_user_mentions_to_db')
RSpec.describe MigrateAllMergeRequestUserMentionsToDb, :migration do
let(:users) { table(:users) }
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
let(:merge_requests) { table(:merge_requests) }
let(:merge_request_user_mentions) { table(:merge_request_user_mentions) }
let(:user) { users.create!(name: 'root', email: 'root@example.com', username: 'root', projects_limit: 0) }
let(:group) { namespaces.create!(name: 'group1', path: 'group1', owner_id: user.id, type: 'Group') }
let(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) }
let(:opened_state) { 1 }
let(:closed_state) { 2 }
let(:merged_state) { 3 }
# migrateable resources
let(:common_args) { { source_branch: 'master', source_project_id: project.id, target_project_id: project.id, author_id: user.id, description: 'mr description with @root mention' } }
let!(:resource1) { merge_requests.create!(common_args.merge(title: "title 1", state_id: opened_state, target_branch: 'feature1')) }
let!(:resource2) { merge_requests.create!(common_args.merge(title: "title 2", state_id: closed_state, target_branch: 'feature2')) }
let!(:resource3) { merge_requests.create!(common_args.merge(title: "title 3", state_id: merged_state, target_branch: 'feature3')) }
# non-migrateable resources
# this merge request is already migrated, as it has a record in the merge_request_user_mentions table
let!(:resource4) { merge_requests.create!(common_args.merge(title: "title 3", state_id: opened_state, target_branch: 'feature4')) }
let!(:user_mention) { merge_request_user_mentions.create!(merge_request_id: resource4.id, mentioned_users_ids: [1]) }
let!(:resource5) { merge_requests.create!(common_args.merge(title: "title 3", description: 'description with no mention', state_id: opened_state, target_branch: 'feature5')) }
it_behaves_like 'schedules resource mentions migration', MergeRequest, false
end

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe SearchController, type: :request do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :public, :repository, :wiki_repo, name: 'awesome project', group: group) }
before_all do
login_as(user)
end
def send_search_request(params)
get search_path, params: params
end
shared_examples 'an efficient database result' do
it 'avoids N+1 database queries' do
create(object, *creation_traits, creation_args)
control = ActiveRecord::QueryRecorder.new(skip_cached: false) { send_search_request(params) }
create_list(object, 3, *creation_traits, creation_args)
expect { send_search_request(params) }.not_to exceed_all_query_limit(control).with_threshold(threshold)
end
end
describe 'GET /search' do
let(:creation_traits) { [] }
context 'for issues scope' do
let(:object) { :issue }
let(:creation_args) { { project: project } }
let(:params) { { search: '*', scope: 'issues' } }
let(:threshold) { 0 }
it_behaves_like 'an efficient database result'
end
context 'for merge_request scope' do
let(:creation_traits) { [:unique_branches] }
let(:object) { :merge_request }
let(:creation_args) { { source_project: project } }
let(:params) { { search: '*', scope: 'merge_requests' } }
let(:threshold) { 0 }
it_behaves_like 'an efficient database result'
end
context 'for project scope' do
let(:creation_traits) { [:public] }
let(:object) { :project }
let(:creation_args) { {} }
let(:params) { { search: '*', scope: 'projects' } }
# some N+1 queries still exist
# each project requires 3 extra queries
# - one count for forks
# - one count for open MRs
# - one count for open Issues
let(:threshold) { 9 }
it_behaves_like 'an efficient database result'
end
end
end

View File

@ -82,3 +82,25 @@ RSpec.shared_examples 'schedules resource mentions migration' do |resource_class
end
end
end
RSpec.shared_examples 'resource migration not run' do |migration_class, resource_class|
it 'does not migrate mentions' do
join = migration_class::JOIN
conditions = migration_class::QUERY_CONDITIONS
expect do
subject.perform(resource_class.name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id))
end.to change { user_mentions.count }.by(0)
end
end
RSpec.shared_examples 'resource notes migration not run' do |migration_class, resource_class|
it 'does not migrate mentions' do
join = migration_class::JOIN
conditions = migration_class::QUERY_CONDITIONS
expect do
subject.perform(resource_class.name, join, conditions, true, Note.minimum(:id), Note.maximum(:id))
end.to change { user_mentions.count }.by(0)
end
end