Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-10-12 12:09:36 +00:00
parent 91e8c3a6ef
commit 57a3a42c88
42 changed files with 435 additions and 219 deletions

View File

@ -172,6 +172,8 @@ review-qa-smoke:
- .review-qa-base
- .review:rules:review-qa-smoke
retry: 1 # This is confusing but this means "2 runs at max".
variables:
QA_RUN_TYPE: review-qa-smoke
script:
- bin/test Test::Instance::Smoke "${CI_ENVIRONMENT_URL}"
@ -180,6 +182,8 @@ review-qa-all:
- .review-qa-base
- .review:rules:review-qa-all
parallel: 5
variables:
QA_RUN_TYPE: review-qa-all
script:
- export KNAPSACK_REPORT_PATH=knapsack/master_report.json
- export KNAPSACK_TEST_FILE_PATTERN=qa/specs/features/**/*_spec.rb

View File

@ -8,7 +8,9 @@ import CiLint from './components/ci_lint.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers),
defaultClient: createDefaultClient(resolvers, {
assumeImmutableResults: true,
}),
});
export default (containerId = '#js-ci-lint') => {

View File

@ -90,17 +90,18 @@ export default {
showMore() {
this.$emit('showMore');
},
generateRowNumber(id) {
generateRowNumber(path, id, index) {
const key = `${path}-${id}-${index}`;
if (!this.glFeatures.lazyLoadCommits) {
return 0;
}
if (!this.rowNumbers[id] && this.rowNumbers[id] !== 0) {
if (!this.rowNumbers[key] && this.rowNumbers[key] !== 0) {
this.$options.totalRowsLoaded += 1;
this.rowNumbers[id] = this.$options.totalRowsLoaded;
this.rowNumbers[key] = this.$options.totalRowsLoaded;
}
return this.rowNumbers[id];
return this.rowNumbers[key];
},
getCommit(fileName, type) {
if (!this.glFeatures.lazyLoadCommits) {
@ -150,7 +151,7 @@ export default {
:lfs-oid="entry.lfsOid"
:loading-path="loadingPath"
:total-entries="totalEntries"
:row-number="generateRowNumber(entry.id)"
:row-number="generateRowNumber(entry.flatPath, entry.id, index)"
:commit-info="getCommit(entry.name, entry.type)"
v-on="$listeners"
/>

View File

@ -35,13 +35,17 @@ export default {
}
if (!this.rulesLeft.length) {
return n__('Requires approval.', 'Requires %d more approvals.', this.approvalsLeft);
return n__(
'Requires %d approval from eligible users.',
'Requires %d approvals from eligible users.',
this.approvalsLeft,
);
}
return sprintf(
n__(
'Requires approval from %{names}.',
'Requires %{count} more approvals from %{names}.',
'Requires %{count} approval from %{names}.',
'Requires %{count} approvals from %{names}.',
this.approvalsLeft,
),
{

View File

@ -71,7 +71,7 @@ export default {
};
</script>
<template>
<div class="d-flex mr-source-target gl-mb-3">
<div class="gl-display-flex mr-source-target">
<mr-widget-icon name="git-merge" />
<div class="git-merge-container d-flex">
<div class="normal">

View File

@ -25,19 +25,15 @@ class Groups::GroupMembersController < Groups::ApplicationController
def index
@sort = params[:sort].presence || sort_value_name
@members = GroupMembersFinder
.new(@group, current_user, params: filter_params)
.execute(include_relations: requested_relations)
if can?(current_user, :admin_group_member, @group)
@skip_groups = @group.related_group_ids
@invited_members = @members.invite
@invited_members = invited_members
@invited_members = @invited_members.search_invite_email(params[:search_invited]) if params[:search_invited].present?
@invited_members = present_invited_members(@invited_members)
end
@members = present_group_members(@members.non_invite)
@members = present_group_members(non_invited_members)
@requesters = present_members(
AccessRequestsFinder.new(@group).execute(current_user)
@ -51,6 +47,20 @@ class Groups::GroupMembersController < Groups::ApplicationController
private
def group_members
@group_members ||= GroupMembersFinder
.new(@group, current_user, params: filter_params)
.execute(include_relations: requested_relations)
end
def invited_members
group_members.invite
end
def non_invited_members
group_members.non_invite
end
def present_invited_members(invited_members)
present_members(invited_members
.page(params[:invited_members_page])

View File

@ -19,16 +19,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_links = @project.project_group_links
@group_links = @group_links.search(params[:search_groups]) if params[:search_groups].present?
project_members = MembersFinder
.new(@project, current_user, params: filter_params)
.execute(include_relations: requested_relations)
if can?(current_user, :admin_project_member, @project)
@invited_members = present_members(project_members.invite)
@invited_members = present_members(invited_members)
@requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user))
end
@project_members = present_members(project_members.non_invite.page(params[:page]))
@project_members = present_members(non_invited_members.page(params[:page]))
@project_member = @project.project_members.new
end
@ -55,6 +51,20 @@ class Projects::ProjectMembersController < Projects::ApplicationController
private
def members
@members ||= MembersFinder
.new(@project, current_user, params: filter_params)
.execute(include_relations: requested_relations)
end
def invited_members
members.invite
end
def non_invited_members
members.non_invite
end
def filter_params
params.permit(:search).merge(sort: @sort)
end

View File

@ -45,6 +45,11 @@ class RegistrationsController < Devise::RegistrationsController
end
def destroy
if current_user.required_terms_not_accepted?
redirect_to profile_account_path, status: :see_other, alert: s_('Profiles|You must accept the Terms of Service in order to perform this action.')
return
end
if destroy_confirmation_valid?
current_user.delete_async(deleted_by: current_user)
session.try(:destroy)

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
module Resolvers
class BoardListResolver < BaseResolver.single
include Gitlab::Graphql::Authorize::AuthorizeResource
include BoardItemFilterable
type Types::BoardListType, null: true
description 'Find an issue board list.'
authorize :read_issue_board_list
argument :id, Types::GlobalIDType[List],
required: true,
description: 'Global ID of the list.'
argument :issue_filters, Types::Boards::BoardIssueInputType,
required: false,
description: 'Filters applied when getting issue metadata in the board list.'
def resolve(id: nil, issue_filters: {})
context.scoped_set!(:issue_filters, item_filters(issue_filters))
Gitlab::Graphql::Lazy.with_value(find_list(id: id)) do |list|
list if authorized_resource?(list)
end
end
private
def find_list(id:)
GitlabSchema.object_from_id(id, expected_type: ::List)
end
end
end

View File

@ -47,6 +47,19 @@ module Types
.metadata
end
end
# board lists have a data dependency on label - so we batch load them here
def title
if object.association(:label).loaded? && object.label_id.present?
object.title
else
loader = Gitlab::Graphql::Loaders::BatchModelLoader.new(Label, object.label_id)
Gitlab::Graphql::Lazy.with_value(loader.find) do |label|
object.label = label
object.title
end
end
end
end
# rubocop: enable Graphql/AuthorizeTypes
end

View File

@ -136,6 +136,10 @@ module Types
complexity: 5,
resolver: ::Resolvers::TimelogResolver
field :board_list, ::Types::BoardListType,
null: true,
resolver: Resolvers::BoardListResolver
def design_management
DesignManagementObject.new(nil)
end

View File

@ -28,8 +28,8 @@ class Environment < ApplicationRecord
has_one :last_deployment, -> { success.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: -> { ::Feature.enabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml) }
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: -> { ::Feature.enabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml) }
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: true
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: true
has_one :upcoming_deployment, -> { running.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
@ -198,14 +198,14 @@ class Environment < ApplicationRecord
# Overriding association
def last_visible_deployable
return super if association_cached?(:last_visible_deployable) || ::Feature.disabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml)
return super if association_cached?(:last_visible_deployable)
last_visible_deployment&.deployable
end
# Overriding association
def last_visible_pipeline
return super if association_cached?(:last_visible_pipeline) || ::Feature.disabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml)
return super if association_cached?(:last_visible_pipeline)
last_visible_deployable&.pipeline
end

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class ListPolicy < BasePolicy # rubocop:disable Gitlab/NamespacedClass
delegate { @subject.board.resource_parent }
end

View File

@ -3,10 +3,15 @@
module Ci
class ArchiveTraceService
def execute(job, worker_name:)
unless job.trace.archival_attempts_available?
Sidekiq.logger.warn(class: worker_name, message: 'The job is out of archival attempts.', job_id: job.id)
job.trace.attempt_archive_cleanup!
return
end
unless job.trace.can_attempt_archival_now?
Sidekiq.logger.warn(class: worker_name,
message: job.trace.archival_attempts_message,
job_id: job.id)
Sidekiq.logger.warn(class: worker_name, message: 'The job can not be archived right now.', job_id: job.id)
return
end

View File

@ -6,7 +6,7 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= _('Configure the %{link} integration.').html_safe % { link: link_to(_('Mailgun events'), 'https://documentation.mailgun.com/en/latest/user_manual.html#webhooks', target: '_blank') }
= _('Configure the %{link} integration.').html_safe % { link: link_to(_('Mailgun events'), 'https://documentation.mailgun.com/en/latest/user_manual.html#webhooks', target: '_blank', rel: 'noopener noreferrer') }
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-mailgun-settings'), html: { class: 'fieldset-form', id: 'mailgun-settings' } do |f|
= form_errors(@application_setting) if expanded

View File

@ -7,7 +7,7 @@
= expanded ? _('Collapse') : _('Expand')
%p
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('development/snowplow/index') }
= html_escape(_('Configure %{link} to track events. %{link_start}Learn more.%{link_end}')) % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank').html_safe, link_start: link_start, link_end: '</a>'.html_safe }
= html_escape(_('Configure %{link} to track events. %{link_start}Learn more.%{link_end}')) % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank', rel: 'noopener noreferrer').html_safe, link_start: link_start, link_end: '</a>'.html_safe }
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-snowplow-settings'), html: { class: 'fieldset-form', id: 'snowplow-settings' } do |f|
= form_errors(@application_setting) if expanded

View File

@ -1,8 +0,0 @@
---
name: environment_last_visible_pipeline_disable_joins
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68870
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340283
milestone: '14.3'
type: development
group: group::release
default_enabled: true

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class CleanupDeleteOrphanedDeploymentsBackgroundMigration < Gitlab::Database::Migration[1.0]
MIGRATION = 'DeleteOrphanedDeployments'
disable_ddl_transaction!
def up
finalize_background_migration(MIGRATION)
end
def down
# no-op
end
end

View File

@ -0,0 +1 @@
5701681a1006584149c88da520f780b186ca32ba1facb8b952252c6d426b6c0d

View File

@ -34,7 +34,7 @@ GET /projects/:id/dependencies?package_manager=yarn,bundler
| Attribute | Type | Required | Description |
| ------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding). |
| `package_manager` | string array | no | Returns dependencies belonging to specified package manager. Valid values: `bundler`, `composer`, `conan`, `maven`, `npm`, `pip` or `yarn`. |
| `package_manager` | string array | no | Returns dependencies belonging to specified package manager. Valid values: `bundler`, `composer`, `conan`, `go`, `maven`, `npm`, `nuget`, `pip`, `yarn`, or `sbt`. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/dependencies"

View File

@ -36,6 +36,19 @@ in [Removed Items](../removed_items.md).
The `Query` type contains the API's top-level entry points for all executable queries.
### `Query.boardList`
Find an issue board list.
Returns [`BoardList`](#boardlist).
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="queryboardlistid"></a>`id` | [`ListID!`](#listid) | Global ID of the list. |
| <a id="queryboardlistissuefilters"></a>`issueFilters` | [`BoardIssueInput`](#boardissueinput) | Filters applied when getting issue metadata in the board list. |
### `Query.ciApplicationSettings`
CI related settings that apply to the entire instance.

View File

@ -275,7 +275,8 @@ You can verify if the MR was deployed to GitLab.com by executing
`/chatops run auto_deploy status <merge_sha>`. To verify existence of
the index, you can:
- Use a meta-command in #database-lab, such as: `\di <index_name>`
- Use a meta-command in #database-lab, such as: `\d <index_name>`
- Ensure that the index is not [`invalid`](https://www.postgresql.org/docs/12/sql-createindex.html#:~:text=The%20psql%20%5Cd%20command%20will%20report%20such%20an%20index%20as%20INVALID)
- Ask someone in #database to check if the index exists
- With proper access, you can also verify directly on production or in a
production clone

View File

@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: howto
---
# Activating GitLab EE
# Activating GitLab EE **(PREMIUM SELF)**
To enable features of GitLab Enterprise Edition (EE), you need to activate your instance. Ensure you are running an enterprise edition. To verify, sign in to GitLab and browse to `/help`. The GitLab edition and version are listed at the top of the **Help** page.
@ -15,7 +15,7 @@ As of GitLab Enterprise Edition 9.4.0, a newly-installed instance without an
uploaded license only has the Free features active. A trial license activates all Ultimate features, but after [the trial expires](#what-happens-when-your-license-expires), some functionality
is locked.
## Activate GitLab EE with an Activation Code **(PREMIUM SELF)**
## Activate GitLab EE with an Activation Code
As of GitLab Enterprise Edition 14.1, you need an activation code to activate your instance. You can obtain an activation code by [purchasing a license](https://about.gitlab.com/pricing/) or by signing up for a [free trial](https://about.gitlab.com/free-trial/). This activation code is a 24-character alphanumeric string you receive in a confirmation email. You can also sign in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in) to copy the activation code to your clipboard.
@ -28,7 +28,7 @@ To begin the activation process with your activation code:
1. Read and accept the terms of service.
1. Select **Activate**.
## Activate GitLab EE with a License File **(PREMIUM SELF)**
## Activate GitLab EE with a License File
If you receive a license file from GitLab (for example a new trial), you can upload it by signing into your GitLab instance as an administrator or adding it during installation. The license is a base64-encoded ASCII text file with a `.gitlab-license` extension.

View File

@ -25,7 +25,7 @@ module Gitlab
delegate :old_trace, to: :job
delegate :can_attempt_archival_now?, :increment_archival_attempts!,
:archival_attempts_message, to: :trace_metadata
:archival_attempts_message, :archival_attempts_available?, to: :trace_metadata
def initialize(job)
@job = job
@ -122,6 +122,10 @@ module Gitlab
end
end
def attempt_archive_cleanup!
destroy_any_orphan_trace_data!
end
def update_interval
if being_watched?
UPDATE_FREQUENCY_WHEN_BEING_WATCHED
@ -191,7 +195,10 @@ module Gitlab
def unsafe_archive!
raise ArchiveError, 'Job is not finished yet' unless job.complete?
unsafe_trace_conditionally_cleanup_before_retry!
already_archived?.tap do |archived|
destroy_any_orphan_trace_data!
raise AlreadyArchivedError, 'Could not archive again' if archived
end
if job.trace_chunks.any?
Gitlab::Ci::Trace::ChunkedIO.new(job) do |stream|
@ -214,16 +221,15 @@ module Gitlab
def already_archived?
# TODO check checksum to ensure archive completed successfully
# See https://gitlab.com/gitlab-org/gitlab/-/issues/259619
trace_artifact.archived_trace_exists?
trace_artifact&.archived_trace_exists?
end
def unsafe_trace_conditionally_cleanup_before_retry!
def destroy_any_orphan_trace_data!
return unless trace_artifact
if already_archived?
# An archive already exists, so make sure to remove the trace chunks
erase_trace_chunks!
raise AlreadyArchivedError, 'Could not archive again'
else
# An archive already exists, but its associated file does not, so remove it
trace_artifact.destroy!

View File

@ -11,6 +11,12 @@ module Gitlab
# balancing is enabled, but no replicas have been configured (= the
# default case).
class PrimaryHost
WAL_ERROR_MESSAGE = <<~MSG.strip
Obtaining WAL information when not using any replicas results in
redundant queries, and may break installations that don't support
streaming replication (e.g. AWS' Aurora database).
MSG
def initialize(load_balancer)
@load_balancer = load_balancer
end
@ -51,30 +57,16 @@ module Gitlab
end
def primary_write_location
@load_balancer.primary_write_location
raise NotImplementedError, WAL_ERROR_MESSAGE
end
def database_replica_location
row = query_and_release(<<-SQL.squish)
SELECT pg_last_wal_replay_lsn()::text AS location
SQL
row['location'] if row.any?
rescue *Host::CONNECTION_ERRORS
nil
raise NotImplementedError, WAL_ERROR_MESSAGE
end
def caught_up?(_location)
true
end
def query_and_release(sql)
connection.select_all(sql).first || {}
rescue StandardError
{}
ensure
release_connection
end
end
end
end

View File

@ -42,6 +42,9 @@ module Gitlab
end
def wal_location_for(load_balancer)
# When only using the primary there's no need for any WAL queries.
return if load_balancer.primary_only?
if ::Gitlab::Database::LoadBalancing::Session.current.use_primary?
load_balancer.primary_write_location
else

View File

@ -104,6 +104,10 @@ module Gitlab
end
def with_primary_write_location
# When only using the primary, there's no point in getting write
# locations, as the primary is always in sync with itself.
return if @load_balancer.primary_only?
location = @load_balancer.primary_write_location
return if location.blank?

View File

@ -26312,6 +26312,9 @@ msgstr ""
msgid "Profiles|You don't have access to delete this user."
msgstr ""
msgid "Profiles|You must accept the Terms of Service in order to perform this action."
msgstr ""
msgid "Profiles|You must transfer ownership or delete groups you are an owner of before you can delete your account"
msgstr ""
@ -28973,13 +28976,13 @@ msgstr ""
msgid "Requirements can be based on users, stakeholders, system, software, or anything else you find important to capture."
msgstr ""
msgid "Requires approval from %{names}."
msgid_plural "Requires %{count} more approvals from %{names}."
msgid "Requires %d approval from eligible users."
msgid_plural "Requires %d approvals from eligible users."
msgstr[0] ""
msgstr[1] ""
msgid "Requires approval."
msgid_plural "Requires %d more approvals."
msgid "Requires %{count} approval from %{names}."
msgid_plural "Requires %{count} approvals from %{names}."
msgstr[0] ""
msgstr[1] ""
@ -40209,6 +40212,9 @@ msgstr ""
msgid "element is not a hierarchy"
msgstr ""
msgid "eligible users"
msgstr ""
msgid "email '%{email}' is not a verified email."
msgstr ""

View File

@ -55,9 +55,9 @@
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.7",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.215.0",
"@gitlab/svgs": "1.218.0",
"@gitlab/tributejs": "1.0.0",
"@gitlab/ui": "32.18.0",
"@gitlab/ui": "32.19.1",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "6.1.4-1",
"@rails/ujs": "6.1.4-1",

View File

@ -20,11 +20,6 @@ module Gitlab
def additional_limits
additional_minutes_usage[%r{([^/ ]+)$}]
end
# TODO: Refactor/Remove this method once https://gitlab.com/gitlab-org/quality/chemlab/-/merge_requests/28 is merged
def additional_minutes_exist?
has_element?(:strong, :additional_minutes, text: 'Additional minutes')
end
end
end
end

View File

@ -602,6 +602,22 @@ RSpec.describe RegistrationsController do
end
end
context 'when user did not accept app terms' do
let(:user) { create(:user, accepted_term: nil) }
before do
stub_application_setting(password_authentication_enabled_for_web: false)
stub_application_setting(password_authentication_enabled_for_git: false)
stub_application_setting(enforce_terms: true)
end
it 'fails with message' do
post :destroy, params: { username: user.username }
expect_failure(s_('Profiles|You must accept the Terms of Service in order to perform this action.'))
end
end
it 'sets the username and caller_id in the context' do
expect(controller).to receive(:destroy).and_wrap_original do |m, *args|
m.call(*args)

View File

@ -61,9 +61,7 @@ describe('MRWidget approvals summary', () => {
it('render message', () => {
const names = toNounSeriesText(testRulesLeft());
expect(wrapper.text()).toContain(
`Requires ${TEST_APPROVALS_LEFT} more approvals from ${names}.`,
);
expect(wrapper.text()).toContain(`Requires ${TEST_APPROVALS_LEFT} approvals from ${names}.`);
});
});
@ -75,7 +73,9 @@ describe('MRWidget approvals summary', () => {
});
it('renders message', () => {
expect(wrapper.text()).toContain(`Requires ${TEST_APPROVALS_LEFT} more approvals.`);
expect(wrapper.text()).toContain(
`Requires ${TEST_APPROVALS_LEFT} approvals from eligible users`,
);
});
});

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::BoardListResolver do
include GraphqlHelpers
include Gitlab::Graphql::Laziness
let_it_be(:guest) { create(:user) }
let_it_be(:unauth_user) { create(:user) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:group_label) { create(:group_label, group: group, name: 'Development') }
let_it_be(:board) { create(:board, resource_parent: group) }
let_it_be(:label_list) { create(:list, board: board, label: group_label) }
describe '#resolve' do
subject { resolve_board_list(args: { id: global_id_of(label_list) }, current_user: current_user) }
context 'with unauthorized user' do
let(:current_user) { unauth_user }
it { is_expected.to be_nil }
end
context 'when authorized' do
let(:current_user) { guest }
before do
group.add_guest(guest)
end
it { is_expected.to eq label_list }
end
end
def resolve_board_list(args: {}, current_user: user)
force(resolve(described_class, obj: nil, args: args, ctx: { current_user: current_user }))
end
end

View File

@ -27,6 +27,7 @@ RSpec.describe GitlabSchema.types['Query'] do
runner
runners
timelogs
board_list
]
expect(described_class).to have_graphql_fields(*expected_fields).at_least
@ -136,4 +137,14 @@ RSpec.describe GitlabSchema.types['Query'] do
is_expected.to have_graphql_resolver(Resolvers::TimelogResolver)
end
end
describe 'boardList field' do
subject { described_class.fields['boardList'] }
it 'finds a board list by its gid' do
is_expected.to have_graphql_arguments(:id, :issue_filters)
is_expected.to have_graphql_type(Types::BoardListType)
is_expected.to have_graphql_resolver(Resolvers::BoardListResolver)
end
end
end

View File

@ -63,9 +63,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::PrimaryHost do
end
describe '#primary_write_location' do
it 'returns the write location of the primary' do
expect(host.primary_write_location).to be_an_instance_of(String)
expect(host.primary_write_location).not_to be_empty
it 'raises NotImplementedError' do
expect { host.primary_write_location }.to raise_error(NotImplementedError)
end
end
@ -76,51 +75,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::PrimaryHost do
end
describe '#database_replica_location' do
let(:connection) { double(:connection) }
it 'returns the write ahead location of the replica', :aggregate_failures do
expect(host)
.to receive(:query_and_release)
.and_return({ 'location' => '0/D525E3A8' })
expect(host.database_replica_location).to be_an_instance_of(String)
end
it 'returns nil when the database query returned no rows' do
expect(host).to receive(:query_and_release).and_return({})
expect(host.database_replica_location).to be_nil
end
it 'returns nil when the database connection fails' do
allow(host).to receive(:connection).and_raise(PG::Error)
expect(host.database_replica_location).to be_nil
end
end
describe '#query_and_release' do
it 'executes a SQL query' do
results = host.query_and_release('SELECT 10 AS number')
expect(results).to be_an_instance_of(Hash)
expect(results['number'].to_i).to eq(10)
end
it 'releases the connection after running the query' do
expect(host)
.to receive(:release_connection)
.once
host.query_and_release('SELECT 10 AS number')
end
it 'returns an empty Hash in the event of an error' do
expect(host.connection)
.to receive(:select_all)
.and_raise(RuntimeError, 'kittens')
expect(host.query_and_release('SELECT 10 AS number')).to eq({})
it 'raises NotImplementedError' do
expect { host.database_replica_location }.to raise_error(NotImplementedError)
end
end
end

View File

@ -21,7 +21,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
middleware.call(worker_class, job, nil, nil) {}
end
describe '#call' do
describe '#call', :database_replica do
shared_context 'data consistency worker class' do |data_consistency, feature_flag|
let(:expected_consistency) { data_consistency }
let(:worker_class) do

View File

@ -188,6 +188,10 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
end
it 'sticks an entity to the primary', :aggregate_failures do
allow(ActiveRecord::Base.connection.load_balancer)
.to receive(:primary_only?)
.and_return(false)
ids.each do |id|
expect(sticking)
.to receive(:set_write_location_for)
@ -199,6 +203,12 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
subject
end
it 'does not update the write location when no replicas are used' do
expect(sticking).not_to receive(:set_write_location_for)
subject
end
end
describe '#stick' do
@ -221,12 +231,22 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
.to receive(:primary_write_location)
.and_return('foo')
allow(ActiveRecord::Base.connection.load_balancer)
.to receive(:primary_only?)
.and_return(false)
expect(sticking)
.to receive(:set_write_location_for)
.with(:user, 42, 'foo')
sticking.mark_primary_write_location(:user, 42)
end
it 'does nothing when no replicas are used' do
expect(sticking).not_to receive(:set_write_location_for)
sticking.mark_primary_write_location(:user, 42)
end
end
describe '#unstick' do

View File

@ -2791,17 +2791,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
extra_update_queries = 4 # transition ... => :canceled, queue pop
extra_generic_commit_status_validation_queries = 2 # name_uniqueness_across_types
# The number of extra load balancing queries depends on whether or not
# we use a load balancer for CI. That in turn depends on the contents of
# database.yml, so here we support both cases.
extra_load_balancer_queries =
if Gitlab::Database.has_config?(:ci)
6
else
3
end
expect(control2.count).to eq(control1.count + extra_update_queries + extra_generic_commit_status_validation_queries + extra_load_balancer_queries)
expect(control2.count).to eq(control1.count + extra_update_queries + extra_generic_commit_status_validation_queries)
end
end

View File

@ -801,38 +801,6 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
expect(query_count).to eq(0)
end
end
context 'when the feature for disable_join is disabled' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:ci_build) { create(:ci_build, project: project, pipeline: pipeline) }
before do
stub_feature_flags(environment_last_visible_pipeline_disable_joins: false)
create(:deployment, :failed, project: project, environment: environment, deployable: ci_build)
end
context 'for preload' do
it 'executes the original association instead of override' do
environment.reload
ActiveRecord::Associations::Preloader.new.preload(environment, [last_visible_deployable: []])
expect_any_instance_of(Deployment).not_to receive(:deployable)
query_count = ActiveRecord::QueryRecorder.new do
expect(subject.id).to eq(ci_build.id)
end.count
expect(query_count).to eq(0)
end
end
context 'for direct call' do
it 'executes the original association instead of override' do
expect_any_instance_of(Deployment).not_to receive(:deployable)
expect(subject.id).to eq(ci_build.id)
end
end
end
end
describe '#last_visible_pipeline' do
@ -963,40 +931,6 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
expect(query_count).to eq(0)
end
end
context 'when the feature for disable_join is disabled' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:ci_build) { create(:ci_build, project: project, pipeline: pipeline) }
before do
stub_feature_flags(environment_last_visible_pipeline_disable_joins: false)
create(:deployment, :failed, project: project, environment: environment, deployable: ci_build)
end
subject { environment.last_visible_pipeline }
context 'for preload' do
it 'executes the original association instead of override' do
environment.reload
ActiveRecord::Associations::Preloader.new.preload(environment, [last_visible_pipeline: []])
expect_any_instance_of(Ci::Build).not_to receive(:pipeline)
query_count = ActiveRecord::QueryRecorder.new do
expect(subject.id).to eq(pipeline.id)
end.count
expect(query_count).to eq(0)
end
end
context 'for direct call' do
it 'executes the original association instead of override' do
expect_any_instance_of(Ci::Build).not_to receive(:pipeline)
expect(subject.id).to eq(pipeline.id)
end
end
end
end
describe '#upcoming_deployment' do

View File

@ -0,0 +1,98 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Querying a Board list' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:board) { create(:board, resource_parent: project) }
let_it_be(:label) { create(:label, project: project, name: 'foo') }
let_it_be(:list) { create(:list, board: board, label: label) }
let_it_be(:issue1) { create(:issue, project: project, labels: [label]) }
let_it_be(:issue2) { create(:issue, project: project, labels: [label], assignees: [current_user]) }
let(:filters) { {} }
let(:query) do
graphql_query_for(
:board_list,
{ id: list.to_global_id.to_s, issueFilters: filters },
%w[title issuesCount]
)
end
subject { graphql_data['boardList'] }
before do
post_graphql(query, current_user: current_user)
end
context 'when the user has access to the list' do
before_all do
project.add_guest(current_user)
end
it_behaves_like 'a working graphql query'
it { is_expected.to include({ 'issuesCount' => 2, 'title' => list.title }) }
context 'with matching issue filters' do
let(:filters) { { assigneeUsername: current_user.username } }
it 'filters issues metadata' do
is_expected.to include({ 'issuesCount' => 1, 'title' => list.title })
end
end
context 'with unmatching issue filters' do
let(:filters) { { assigneeUsername: 'foo' } }
it 'filters issues metadata' do
is_expected.to include({ 'issuesCount' => 0, 'title' => list.title })
end
end
end
context 'when the user does not have access to the list' do
it { is_expected.to be_nil }
end
context 'when ID argument is missing' do
let(:query) do
graphql_query_for('boardList', {}, 'title')
end
it 'raises an exception' do
expect(graphql_errors).to include(a_hash_including('message' => "Field 'boardList' is missing required arguments: id"))
end
end
context 'when list ID is not found' do
let(:query) do
graphql_query_for('boardList', { id: "gid://gitlab/List/#{non_existing_record_id}" }, 'title')
end
it { is_expected.to be_nil }
end
it 'does not have an N+1 performance issue' do
a, b = create_list(:list, 2, board: board)
ctx = { current_user: current_user }
project.add_guest(current_user)
baseline = graphql_query_for(:board_list, { id: global_id_of(a) }, 'title')
query = <<~GQL
query {
a: #{query_graphql_field(:board_list, { id: global_id_of(a) }, 'title')}
b: #{query_graphql_field(:board_list, { id: global_id_of(b) }, 'title')}
}
GQL
control = ActiveRecord::QueryRecorder.new do
run_with_clean_state(baseline, context: ctx)
end
expect { run_with_clean_state(query, context: ctx) }.not_to exceed_query_limit(control)
end
end

View File

@ -88,6 +88,32 @@ RSpec.describe Ci::ArchiveTraceService, '#execute' do
subject
end
context 'job has archive and chunks' do
let(:job) { create(:ci_build, :success, :trace_artifact) }
before do
create(:ci_build_trace_chunk, build: job, chunk_index: 0)
end
context 'archive is not completed' do
before do
job.job_artifacts_trace.file.remove!
end
it 'cleanups any stale archive data' do
expect(job.job_artifacts_trace).to be_present
subject
expect(job.reload.job_artifacts_trace).to be_nil
end
end
it 'removes trace chunks' do
expect { subject }.to change { job.trace_chunks.count }.to(0)
end
end
end
context 'when the archival process is backed off' do

View File

@ -904,23 +904,23 @@
stylelint-declaration-strict-value "1.7.7"
stylelint-scss "3.18.0"
"@gitlab/svgs@1.215.0":
version "1.215.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.215.0.tgz#f2760bbb0a38b26346e1b755e63fb63eba005edd"
integrity sha512-/bc0+EOYPQlPCMbfyOkMLxDKBn+ewEBlmTRmFwf7mXvfIRszdJPY8XCx/fJIEQwDr8+k4E28ktFnLZGnaFhCnw==
"@gitlab/svgs@1.218.0":
version "1.218.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.218.0.tgz#0715e2ef50b5cb83813e1a5e29d5919a96685734"
integrity sha512-eckixyumeWogykEUZfP4pGjoRdhdWQIFwSTM0ks5tQqza+BikcL2xvxzicJs69T1IiCKwYtEpR1c3T/hSx39Mg==
"@gitlab/tributejs@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
"@gitlab/ui@32.18.0":
version "32.18.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.18.0.tgz#cd340f050fe0183218f6233328aca2369bd6e449"
integrity sha512-bDMmsNB9VMBX2JbezyJWfk02t0aFfAT9Ez4ALTDUJLb5/Q9GKByfE5sLycms6L1aZxzP6r1jypnu5DD0eT92eg==
"@gitlab/ui@32.19.1":
version "32.19.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.19.1.tgz#ab54408272cb5ee695dc0a328892e047da3d41ac"
integrity sha512-ooc0TwCvREuWJfvn8EbOkEz1Mh4UKEu7x0MKhD+TBjG+JJwLKDClmD1cPPE05BXtWAvW5W9JUBkaeMCVQG2l3g==
dependencies:
"@babel/standalone" "^7.0.0"
bootstrap-vue "2.19.0"
bootstrap-vue "2.20.1"
copy-to-clipboard "^3.0.8"
dompurify "^2.3.3"
echarts "^4.9.0"
@ -2758,10 +2758,10 @@ boolbase@^1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
bootstrap-vue@2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/bootstrap-vue/-/bootstrap-vue-2.19.0.tgz#5019df48251e552a5c34da57fc97dabebd53b02f"
integrity sha512-IjAXUSrRU5Qu9x3uwUcoj6LtysKbCVeWoJOsODyI/WokStUr95M+tTIajXUjIrB/Nsk0fS+RNvZnm2sWeNFrhg==
bootstrap-vue@2.20.1:
version "2.20.1"
resolved "https://registry.yarnpkg.com/bootstrap-vue/-/bootstrap-vue-2.20.1.tgz#1b6cd4368632c1a6dd4a5ed161242baa131c3cd5"
integrity sha512-s+w83q0T2mo/RbFwTM8gExbLJMEOYpdTUqmyFaHv2Ir+TFprMvTWpeAzeNuawJ130W1gePZ3LW3cNp1t/tZbOw==
dependencies:
"@nuxt/opencollective" "^0.3.2"
bootstrap ">=4.5.3 <5.0.0"