Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-22 00:08:07 +00:00
parent e7bc93852d
commit 21539fe9ab
50 changed files with 1430 additions and 468 deletions

View file

@ -343,7 +343,7 @@ group :development do
end
group :development, :test do
gem 'bullet', '~> 6.0.2', require: !!ENV['ENABLE_BULLET']
gem 'bullet', '~> 6.0.2'
gem 'pry-byebug', '~> 3.5.1', platform: :mri
gem 'pry-rails', '~> 0.3.9'

View file

@ -1,8 +1,7 @@
import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import eventHub from '~/frequent_items/event_hub';
import frequentItems from './components/app.vue';
import eventHub from './event_hub';
Vue.use(Translate);
@ -17,7 +16,7 @@ const frequentItemDropdowns = [
},
];
const initFrequentItemDropdowns = () => {
export default function initFrequentItemDropdowns() {
frequentItemDropdowns.forEach(dropdown => {
const { namespace, key } = dropdown;
const el = document.getElementById(`js-${namespace}-dropdown`);
@ -29,45 +28,40 @@ const initFrequentItemDropdowns = () => {
return;
}
$(navEl).on('shown.bs.dropdown', () => {
eventHub.$emit(`${namespace}-dropdownOpen`);
});
$(navEl).on('shown.bs.dropdown', () =>
import('./components/app.vue').then(({ default: FrequentItems }) => {
// eslint-disable-next-line no-new
new Vue({
el,
data() {
const { dataset } = this.$options.el;
const item = {
id: Number(dataset[`${key}Id`]),
name: dataset[`${key}Name`],
namespace: dataset[`${key}Namespace`],
webUrl: dataset[`${key}WebUrl`],
avatarUrl: dataset[`${key}AvatarUrl`] || null,
lastAccessedOn: Date.now(),
};
// eslint-disable-next-line no-new
new Vue({
el,
components: {
frequentItems,
},
data() {
const { dataset } = this.$options.el;
const item = {
id: Number(dataset[`${key}Id`]),
name: dataset[`${key}Name`],
namespace: dataset[`${key}Namespace`],
webUrl: dataset[`${key}WebUrl`],
avatarUrl: dataset[`${key}AvatarUrl`] || null,
lastAccessedOn: Date.now(),
};
return {
currentUserName: dataset.userName,
currentItem: item,
};
},
render(createElement) {
return createElement('frequent-items', {
props: {
namespace,
currentUserName: this.currentUserName,
currentItem: this.currentItem,
return {
currentUserName: dataset.userName,
currentItem: item,
};
},
render(createElement) {
return createElement(FrequentItems, {
props: {
namespace,
currentUserName: this.currentUserName,
currentItem: this.currentItem,
},
});
},
});
},
});
});
};
document.addEventListener('DOMContentLoaded', () => {
requestIdleCallback(initFrequentItemDropdowns);
});
eventHub.$emit(`${namespace}-dropdownOpen`);
}),
);
});
}

View file

@ -28,7 +28,7 @@ import initLayoutNav from './layout_nav';
import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo';
import './frequent_items';
import initFrequentItemDropdowns from './frequent_items';
import initBreadcrumbs from './breadcrumb';
import initUsagePingConsent from './usage_ping_consent';
import initPerformanceBar from './performance_bar';
@ -107,6 +107,7 @@ function deferredInitialisation() {
initUsagePingConsent();
initUserPopovers();
initBroadcastNotifications();
initFrequentItemDropdowns();
const recoverySettingsCallout = document.querySelector('.js-recovery-settings-callout');
PersistentUserCallout.factory(recoverySettingsCallout);

View file

@ -39,6 +39,11 @@ export default {
metricDetails() {
return this.currentRequest.details[this.metric];
},
metricDetailsLabel() {
return this.metricDetails.duration
? `${this.metricDetails.duration} / ${this.metricDetails.calls}`
: this.metricDetails.calls;
},
detailsList() {
return this.metricDetails.details;
},
@ -68,7 +73,7 @@ export default {
type="button"
data-toggle="modal"
>
{{ metricDetails.duration }} / {{ metricDetails.calls }}
{{ metricDetailsLabel }}
</button>
<gl-modal
:id="`modal-peek-${metric}-details`"
@ -80,7 +85,9 @@ export default {
<template v-if="detailsList.length">
<tr v-for="(item, index) in detailsList" :key="index">
<td>
<span>{{ sprintf(__('%{duration}ms'), { duration: item.duration }) }}</span>
<span v-if="item.duration">{{
sprintf(__('%{duration}ms'), { duration: item.duration })
}}</span>
</td>
<td>
<div class="js-toggle-container">

View file

@ -37,6 +37,11 @@ export default {
header: s__('PerformanceBar|SQL queries'),
keys: ['sql'],
},
{
metric: 'bullet',
header: s__('PerformanceBar|Bullet notifications'),
keys: ['notification'],
},
{
metric: 'gitaly',
header: s__('PerformanceBar|Gitaly calls'),

View file

@ -109,7 +109,7 @@ export default {
<evidence-block v-if="hasEvidence && shouldShowEvidence" :release="release" />
<div ref="gfm-content" class="card-text prepend-top-default">
<div v-html="release.descriptionHtml"></div>
<div class="md" v-html="release.descriptionHtml"></div>
</div>
</div>

View file

@ -7,8 +7,15 @@ module Mutations
def resolve_issuable(type:, parent_path:, iid:)
parent = resolve_issuable_parent(type, parent_path)
key = type == :merge_request ? :iids : :iid
args = { key => iid.to_s }
issuable_resolver(type, parent, context).resolve(iid: iid.to_s)
resolver = issuable_resolver(type, parent, context)
ready, early_return = resolver.ready?(**args)
return early_return unless ready
resolver.resolve(**args)
end
private

View file

@ -0,0 +1,50 @@
# frozen_string_literal: true
# Mixin for resolving merge requests. All arguments must be in forms
# that `MergeRequestsFinder` can handle, so you may need to use aliasing.
module ResolvesMergeRequests
extend ActiveSupport::Concern
included do
type Types::MergeRequestType, null: true
end
def resolve(**args)
args[:iids] = Array.wrap(args[:iids]) if args[:iids]
args.compact!
if args.keys == [:iids]
batch_load_merge_requests(args[:iids])
else
args[:project_id] = project.id
MergeRequestsFinder.new(current_user, args).execute
end.then(&(single? ? :first : :itself))
end
def ready?(**args)
return early_return if no_results_possible?(args)
super
end
def early_return
[false, single? ? nil : MergeRequest.none]
end
private
def batch_load_merge_requests(iids)
iids.map { |iid| batch_load(iid) }.select(&:itself) # .compact doesn't work on BatchLoader
end
# rubocop: disable CodeReuse/ActiveRecord
def batch_load(iid)
BatchLoader::GraphQL.for(iid.to_s).batch(key: project) do |iids, loader, args|
args[:key].merge_requests.where(iid: iids).each do |mr|
loader.call(mr.iid.to_s, mr)
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Resolvers
class MergeRequestResolver < BaseResolver.single
include ResolvesMergeRequests
alias_method :project, :synchronized_object
argument :iid, GraphQL::STRING_TYPE,
required: true,
as: :iids,
description: 'IID of the merge request, for example `1`'
def no_results_possible?(args)
project.nil?
end
end
end

View file

@ -2,47 +2,39 @@
module Resolvers
class MergeRequestsResolver < BaseResolver
argument :iid, GraphQL::STRING_TYPE,
required: false,
description: 'IID of the merge request, for example `1`'
include ResolvesMergeRequests
alias_method :project, :synchronized_object
argument :iids, [GraphQL::STRING_TYPE],
required: false,
description: 'Array of IIDs of merge requests, for example `[1, 2]`'
type Types::MergeRequestType, null: true
argument :source_branches, [GraphQL::STRING_TYPE],
required: false,
as: :source_branch,
description: 'Array of source branch names. All resolved merge requests will have one of these branches as their source.'
alias_method :project, :object
argument :target_branches, [GraphQL::STRING_TYPE],
required: false,
as: :target_branch,
description: 'Array of target branch names. All resolved merge requests will have one of these branches as their target.'
def resolve(**args)
project = object.respond_to?(:sync) ? object.sync : object
return MergeRequest.none if project.nil?
argument :state, ::Types::MergeRequestStateEnum,
required: false,
description: 'A merge request state. If provided, all resolved merge requests will have this state.'
args[:iids] ||= [args[:iid]].compact
argument :labels, [GraphQL::STRING_TYPE],
required: false,
as: :label_name,
description: 'Array of label names. All resolved merge requests will have all of these labels.'
if args[:iids].any?
batch_load_merge_requests(args[:iids])
else
args[:project_id] = project.id
MergeRequestsFinder.new(context[:current_user], args).execute
end
def self.single
::Resolvers::MergeRequestResolver
end
def batch_load_merge_requests(iids)
iids.map { |iid| batch_load(iid) }.select(&:itself) # .compact doesn't work on BatchLoader
def no_results_possible?(args)
project.nil? || args.values.any? { |v| v.is_a?(Array) && v.empty? }
end
# rubocop: disable CodeReuse/ActiveRecord
def batch_load(iid)
BatchLoader::GraphQL.for(iid.to_s).batch(key: project) do |iids, loader, args|
arg_key = args[:key].respond_to?(:sync) ? args[:key].sync : args[:key]
arg_key.merge_requests.where(iid: iids).each do |mr|
loader.call(mr.iid.to_s, mr)
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
end

View file

@ -81,8 +81,14 @@ module Types
description: 'Default merge commit message of the merge request'
field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false,
description: 'Indicates if a merge is currently occurring'
field :source_branch_exists, GraphQL::BOOLEAN_TYPE, method: :source_branch_exists?, null: false,
field :source_branch_exists, GraphQL::BOOLEAN_TYPE,
null: false, calls_gitaly: true,
method: :source_branch_exists?,
description: 'Indicates if the source branch of the merge request exists'
field :target_branch_exists, GraphQL::BOOLEAN_TYPE,
null: false, calls_gitaly: true,
method: :target_branch_exists?,
description: 'Indicates if the target branch of the merge request exists'
field :mergeable_discussions_state, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged'
field :web_url, GraphQL::STRING_TYPE, null: true,

View file

@ -3,6 +3,11 @@
module Types
module PermissionTypes
class MergeRequest < BasePermissionType
PERMISSION_FIELDS = %i[push_to_source_branch
remove_source_branch
cherry_pick_on_current_merge_request
revert_on_current_merge_request].freeze
present_using MergeRequestPresenter
description 'Check permissions for the current user on a merge request'
graphql_name 'MergeRequestPermissions'
@ -10,10 +15,9 @@ module Types
abilities :read_merge_request, :admin_merge_request,
:update_merge_request, :create_note
permission_field :push_to_source_branch, method: :can_push_to_source_branch?, calls_gitaly: true
permission_field :remove_source_branch, method: :can_remove_source_branch?, calls_gitaly: true
permission_field :cherry_pick_on_current_merge_request, method: :can_cherry_pick_on_current_merge_request?
permission_field :revert_on_current_merge_request, method: :can_revert_on_current_merge_request?
PERMISSION_FIELDS.each do |field_name|
permission_field field_name, method: :"can_#{field_name}?", calls_gitaly: true
end
end
end
end

View file

@ -49,6 +49,8 @@ module Releases
notify_create_release(release)
create_evidence!(release)
success(tag: tag, release: release)
rescue => e
error(e.message, 400)
@ -70,5 +72,15 @@ module Releases
milestones: milestones
)
end
def create_evidence!(release)
return if release.historical_release?
if release.upcoming_release?
CreateEvidenceWorker.perform_at(release.released_at, release.id)
else
CreateEvidenceWorker.perform_async(release.id)
end
end
end
end

View file

@ -1,5 +1,5 @@
.gitlab-promo
= link_to 'Homepage', promo_url
= link_to 'Blog', promo_url + '/blog/'
= link_to _('Homepage'), promo_url
= link_to _('Blog'), promo_url + '/blog/'
= link_to '@gitlab', 'https://twitter.com/gitlab'
= link_to 'Requests', 'https://gitlab.com/gitlab-org/gitlab-foss/blob/master/CONTRIBUTING.md#feature-proposals'
= link_to _('Requests'), 'https://gitlab.com/gitlab-org/gitlab-foss/blob/master/CONTRIBUTING.md#feature-proposals'

View file

@ -0,0 +1,5 @@
---
title: Resolve image overflow at releases list panel
merge_request: 32307
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Fix creating release evidence if release is created via UI
merge_request: 32441
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Externalize i18n strings from ./app/views/shared/_promo.html.haml
merge_request: 32109
author: Gilang Gumilar
type: changed

View file

@ -0,0 +1,5 @@
---
title: Add filters to merge request fields
merge_request: 32328
author:
type: added

View file

@ -1,10 +1,15 @@
if defined?(Bullet) && ENV['ENABLE_BULLET']
def bullet_enabled?
Gitlab::Utils.to_boolean(ENV['ENABLE_BULLET'].to_s)
end
if defined?(Bullet) && (bullet_enabled? || Rails.env.development?)
Rails.application.configure do
config.after_initialize do
Bullet.enable = true
Bullet.bullet_logger = true
Bullet.console = true
Bullet.bullet_logger = bullet_enabled?
Bullet.console = bullet_enabled?
Bullet.raise = Rails.env.test?
end
end

View file

@ -10,5 +10,6 @@ Peek.into Peek::Views::ActiveRecord
Peek.into Peek::Views::Gitaly
Peek.into Peek::Views::RedisDetailed
Peek.into Peek::Views::Rugged
Peek.into Peek::Views::BulletDetailed if defined?(Bullet)
Peek.into Peek::Views::Tracing if Labkit::Tracing.tracing_url_enabled?

View file

@ -6005,6 +6005,11 @@ type MergeRequest implements Noteable {
"""
targetBranch: String!
"""
Indicates if the target branch of the merge request exists
"""
targetBranchExists: Boolean!
"""
Target project of the merge request
"""
@ -7871,12 +7876,7 @@ type Project {
"""
IID of the merge request, for example `1`
"""
iid: String
"""
Array of IIDs of merge requests, for example `[1, 2]`
"""
iids: [String!]
iid: String!
): MergeRequest
"""
@ -7898,20 +7898,35 @@ type Project {
"""
first: Int
"""
IID of the merge request, for example `1`
"""
iid: String
"""
Array of IIDs of merge requests, for example `[1, 2]`
"""
iids: [String!]
"""
Array of label names. All resolved merge requests will have all of these labels.
"""
labels: [String!]
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
Array of source branch names. All resolved merge requests will have one of these branches as their source.
"""
sourceBranches: [String!]
"""
A merge request state. If provided, all resolved merge requests will have this state.
"""
state: MergeRequestState
"""
Array of target branch names. All resolved merge requests will have one of these branches as their target.
"""
targetBranches: [String!]
): MergeRequestConnection
"""

View file

@ -16717,6 +16717,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "targetBranchExists",
"description": "Indicates if the target branch of the merge request exists",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "targetProject",
"description": "Target project of the merge request",
@ -23270,26 +23288,12 @@
"name": "iid",
"description": "IID of the merge request, for example `1`",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "iids",
"description": "Array of IIDs of merge requests, for example `[1, 2]`",
"type": {
"kind": "LIST",
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
@ -23308,18 +23312,72 @@
"description": "Merge requests of the project",
"args": [
{
"name": "iid",
"description": "IID of the merge request, for example `1`",
"name": "iids",
"description": "Array of IIDs of merge requests, for example `[1, 2]`",
"type": {
"kind": "SCALAR",
"name": "String",
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "sourceBranches",
"description": "Array of source branch names. All resolved merge requests will have one of these branches as their source.",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "targetBranches",
"description": "Array of target branch names. All resolved merge requests will have one of these branches as their target.",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "state",
"description": "A merge request state. If provided, all resolved merge requests will have this state.",
"type": {
"kind": "ENUM",
"name": "MergeRequestState",
"ofType": null
},
"defaultValue": null
},
{
"name": "iids",
"description": "Array of IIDs of merge requests, for example `[1, 2]`",
"name": "labels",
"description": "Array of label names. All resolved merge requests will have all of these labels.",
"type": {
"kind": "LIST",
"name": null,

View file

@ -886,6 +886,7 @@ Autogenerated return type of MarkAsSpamSnippet
| `state` | MergeRequestState! | State of the merge request |
| `subscribed` | Boolean! | Indicates if the currently logged in user is subscribed to this merge request |
| `targetBranch` | String! | Target branch of the merge request |
| `targetBranchExists` | Boolean! | Indicates if the target branch of the merge request exists |
| `targetProject` | Project! | Target project of the merge request |
| `targetProjectId` | Int! | ID of the merge request target project |
| `taskCompletionStatus` | TaskCompletionStatus! | Completion status of tasks |

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

View file

@ -107,9 +107,13 @@ Recorded transactions can be found by navigating to `/sherlock/transactions`.
## Bullet
Bullet is a Gem that can be used to track down N+1 query problems. Because
Bullet adds quite a bit of logging noise it's disabled by default. To enable
Bullet, set the environment variable `ENABLE_BULLET` to a non-empty value before
Bullet is a Gem that can be used to track down N+1 query problems. Bullet section is
displayed on the [performance-bar](../administration/monitoring/performance/performance_bar.md).
![Bullet](img/bullet_v13_0.png)
Because Bullet adds quite a bit of logging noise the logging is disabled by default.
To enable the logging, set the environment variable `ENABLE_BULLET` to a non-empty value before
starting GitLab. For example:
```shell

View file

@ -67,7 +67,6 @@ module API
if result[:status] == :success
log_release_created_audit_event(result[:release])
create_evidence!
present result[:release], with: Entities::Release, current_user: current_user
else
@ -169,16 +168,6 @@ module API
def log_release_milestones_updated_audit_event
# This is a separate method so that EE can extend its behaviour
end
def create_evidence!
return if release.historical_release?
if release.upcoming_release?
CreateEvidenceWorker.perform_at(release.released_at, release.id) # rubocop:disable CodeReuse/Worker
else
CreateEvidenceWorker.perform_async(release.id) # rubocop:disable CodeReuse/Worker
end
end
end
end
end

View file

@ -84,7 +84,7 @@ module Gitlab
elsif resolved_type.is_a? Array
# A simple list of rendered types each object being an object to authorize
resolved_type.select do |single_object_type|
allowed_access?(current_user, single_object_type.object)
allowed_access?(current_user, unpromise(single_object_type).object)
end
else
raise "Can't authorize #{@field}"
@ -113,6 +113,17 @@ module Gitlab
def scalar_type?
node_type_for_basic_connection(@field.type).kind.scalar?
end
# Sometimes we get promises, and have to resolve them. The dedicated way
# of doing this (GitlabSchema.after_lazy) is a private framework method,
# and so we use duck-typing interface inference here instead.
def unpromise(maybe_promise)
if maybe_promise.respond_to?(:value) && !maybe_promise.respond_to?(:object)
maybe_promise.value
else
maybe_promise
end
end
end
end
end

View file

@ -1,27 +1,23 @@
# frozen_string_literal: true
# For hardening usage ping and make it easier to add measures there is in place
# * alt_usage_data method
# handles StandardError and fallbacks into -1 this way not all measures fail if we encounter one exception
# When developing usage data metrics use the below usage data interface methods
# unless you have good reasons to implement custom usage data
# See `lib/gitlab/utils/usage_data.rb`
#
# Examples:
# alt_usage_data { Gitlab::VERSION }
# alt_usage_data { Gitlab::CurrentSettings.uuid }
#
# * redis_usage_data method
# handles ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent
# returns -1 when a block is sent or hash with all values -1 when a counter is sent
# different behaviour due to 2 different implementations of redis counter
#
# Examples:
# redis_usage_data(Gitlab::UsageDataCounters::WikiPageCounter)
# redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] }
# Examples
# issues_using_zoom_quick_actions: distinct_count(ZoomMeeting, :issue_id),
# active_user_count: count(User.active)
# alt_usage_data { Gitlab::VERSION }
# redis_usage_data(Gitlab::UsageDataCounters::WikiPageCounter)
# redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] }
module Gitlab
class UsageData
BATCH_SIZE = 100
FALLBACK = -1
class << self
include Gitlab::Utils::UsageData
def data(force_refresh: false)
Rails.cache.fetch('usage_data', force: force_refresh, expires_in: 2.weeks) do
uncached_data
@ -386,58 +382,6 @@ module Gitlab
{} # augmented in EE
end
def count(relation, column = nil, batch: true, start: nil, finish: nil)
if batch && Feature.enabled?(:usage_ping_batch_counter, default_enabled: true)
Gitlab::Database::BatchCount.batch_count(relation, column, start: start, finish: finish)
else
relation.count
end
rescue ActiveRecord::StatementInvalid
FALLBACK
end
def distinct_count(relation, column = nil, batch: true, start: nil, finish: nil)
if batch && Feature.enabled?(:usage_ping_batch_counter, default_enabled: true)
Gitlab::Database::BatchCount.batch_distinct_count(relation, column, start: start, finish: finish)
else
relation.distinct_count_by(column)
end
rescue ActiveRecord::StatementInvalid
FALLBACK
end
def alt_usage_data(value = nil, fallback: FALLBACK, &block)
if block_given?
yield
else
value
end
rescue
fallback
end
def redis_usage_data(counter = nil, &block)
if block_given?
redis_usage_counter(&block)
elsif counter.present?
redis_usage_data_totals(counter)
end
end
private
def redis_usage_counter
yield
rescue ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent
FALLBACK
end
def redis_usage_data_totals(counter)
counter.totals
rescue ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent
counter.fallback_totals
end
def installation_type
if Rails.env.production?
Gitlab::INSTALLATION_TYPE

View file

@ -0,0 +1,93 @@
# frozen_string_literal: true
# Usage data utilities
#
# * distinct_count(relation, column = nil, batch: true, start: nil, finish: nil)
# Does a distinct batch count, smartly reduces batch_size and handles errors
#
# Examples:
# issues_using_zoom_quick_actions: distinct_count(ZoomMeeting, :issue_id),
#
# * count(relation, column = nil, batch: true, start: nil, finish: nil)
# Does a non-distinct batch count, smartly reduces batch_size and handles errors
#
# Examples:
# active_user_count: count(User.active)
#
# * alt_usage_data method
# handles StandardError and fallbacks into -1 this way not all measures fail if we encounter one exception
#
# Examples:
# alt_usage_data { Gitlab::VERSION }
# alt_usage_data { Gitlab::CurrentSettings.uuid }
#
# * redis_usage_data method
# handles ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent
# returns -1 when a block is sent or hash with all values -1 when a counter is sent
# different behaviour due to 2 different implementations of redis counter
#
# Examples:
# redis_usage_data(Gitlab::UsageDataCounters::WikiPageCounter)
# redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] }
module Gitlab
module Utils
module UsageData
extend self
FALLBACK = -1
def count(relation, column = nil, batch: true, start: nil, finish: nil)
if batch && Feature.enabled?(:usage_ping_batch_counter, default_enabled: true)
Gitlab::Database::BatchCount.batch_count(relation, column, start: start, finish: finish)
else
relation.count
end
rescue ActiveRecord::StatementInvalid
FALLBACK
end
def distinct_count(relation, column = nil, batch: true, start: nil, finish: nil)
if batch && Feature.enabled?(:usage_ping_batch_counter, default_enabled: true)
Gitlab::Database::BatchCount.batch_distinct_count(relation, column, start: start, finish: finish)
else
relation.distinct_count_by(column)
end
rescue ActiveRecord::StatementInvalid
FALLBACK
end
def alt_usage_data(value = nil, fallback: FALLBACK, &block)
if block_given?
yield
else
value
end
rescue
fallback
end
def redis_usage_data(counter = nil, &block)
if block_given?
redis_usage_counter(&block)
elsif counter.present?
redis_usage_data_totals(counter)
end
end
private
def redis_usage_counter
yield
rescue ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent
FALLBACK
end
def redis_usage_data_totals(counter)
counter.totals
rescue ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent
counter.fallback_totals
end
end
end
end

View file

@ -0,0 +1,47 @@
# frozen_string_literal: true
module Peek
module Views
class BulletDetailed < DetailedView
WARNING_MESSAGE = "Unoptimized queries detected"
def key
'bullet'
end
def results
return {} unless ::Bullet.enable?
return {} unless calls > 0
{
calls: calls,
details: details,
warnings: [WARNING_MESSAGE]
}
end
private
def details
notifications.map do |notification|
# there is no public method which returns pure backtace:
# https://github.com/flyerhzm/bullet/blob/9cda9c224a46786ecfa894480c4dd4d304db2adb/lib/bullet/notification/n_plus_one_query.rb
backtrace = notification.body_with_caller
{
notification: "#{notification.title}: #{notification.body}",
backtrace: backtrace
}
end
end
def calls
notifications.size
end
def notifications
::Bullet.notification_collector&.collection || []
end
end
end
end

View file

@ -0,0 +1,24 @@
namespace :gitlab do
namespace :container_registry do
desc "GitLab | Container Registry | Configure"
task configure: :gitlab_environment do
registry_config = Gitlab.config.registry
unless registry_config.enabled && registry_config.api_url.presence
raise 'Registry is not enabled or registry api url is not present.'
end
warn_user_is_not_gitlab
url = registry_config.api_url
client = ContainerRegistry::Client.new(url)
info = client.registry_info
Gitlab::CurrentSettings.update!(
container_registry_vendor: info[:vendor] || '',
container_registry_version: info[:version] || '',
container_registry_features: info[:features] || []
)
end
end
end

View file

@ -11263,6 +11263,9 @@ msgstr ""
msgid "History of authentications"
msgstr ""
msgid "Homepage"
msgstr ""
msgid "Hook execution failed. Ensure the group has a project with commits."
msgstr ""
@ -15371,6 +15374,9 @@ msgstr ""
msgid "Performance optimization"
msgstr ""
msgid "PerformanceBar|Bullet notifications"
msgstr ""
msgid "PerformanceBar|Download"
msgstr ""
@ -18170,6 +18176,9 @@ msgstr ""
msgid "Requested states are invalid"
msgstr ""
msgid "Requests"
msgstr ""
msgid "Requests Profiles"
msgstr ""

View file

@ -133,6 +133,11 @@ FactoryBot.define do
end
end
trait :unique_branches do
source_branch { generate(:branch) }
target_branch { generate(:branch) }
end
trait :with_coverage_reports do
after(:build) do |merge_request|
merge_request.head_pipeline = build(

View file

@ -6,6 +6,24 @@ describe MergeRequestsFinder do
context "multiple projects with merge requests" do
include_context 'MergeRequestsFinder multiple projects with merge requests context'
shared_examples 'scalar or array parameter' do
let(:values) { merge_requests.pluck(attribute) }
let(:params) { {} }
let(:key) { attribute }
it 'takes scalar values' do
found = described_class.new(user, params.merge(key => values.first)).execute
expect(found).to contain_exactly(merge_requests.first)
end
it 'takes array values' do
found = described_class.new(user, params.merge(key => values)).execute
expect(found).to match_array(merge_requests)
end
end
describe '#execute' do
it 'filters by scope' do
params = { scope: 'authored', state: 'opened' }
@ -91,28 +109,56 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request5)
end
it 'filters by iid' do
params = { project_id: project1.id, iids: merge_request1.iid }
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request1)
describe ':iid parameter' do
it_behaves_like 'scalar or array parameter' do
let(:params) { { project_id: project1.id } }
let(:merge_requests) { [merge_request1, merge_request2] }
let(:key) { :iids }
let(:attribute) { :iid }
end
end
it 'filters by source branch' do
params = { source_branch: merge_request2.source_branch }
[:source_branch, :target_branch].each do |param|
describe "#{param} parameter" do
let(:merge_requests) { create_list(:merge_request, 2, :unique_branches, source_project: project4, target_project: project4, author: user) }
let(:attribute) { param }
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request2)
it_behaves_like 'scalar or array parameter'
end
end
it 'filters by target branch' do
params = { target_branch: merge_request2.target_branch }
describe ':label_name parameter' do
let(:common_labels) { create_list(:label, 3) }
let(:distinct_labels) { create_list(:label, 3) }
let(:merge_requests) do
common_attrs = {
source_project: project1, target_project: project1, author: user
}
distinct_labels.map do |label|
labels = [label, *common_labels]
create(:labeled_merge_request, :closed, labels: labels, **common_attrs)
end
end
merge_requests = described_class.new(user, params).execute
def find(label_name)
described_class.new(user, label_name: label_name).execute
end
expect(merge_requests).to contain_exactly(merge_request2)
it 'accepts a single label' do
found = find(distinct_labels.first.title)
common = find(common_labels.first.title)
expect(found).to contain_exactly(merge_requests.first)
expect(common).to match_array(merge_requests)
end
it 'accepts an array of labels, all of which must match' do
all_distinct = find(distinct_labels.pluck(:title))
all_common = find(common_labels.pluck(:title))
expect(all_distinct).to be_empty
expect(all_common).to match_array(merge_requests)
end
end
it 'filters by source project id' do
@ -158,7 +204,10 @@ describe MergeRequestsFinder do
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request4, merge_request5, wip_merge_request1, wip_merge_request2, wip_merge_request3, wip_merge_request4)
expect(merge_requests).to contain_exactly(
merge_request1, merge_request2, merge_request3, merge_request4,
merge_request5, wip_merge_request1, wip_merge_request2, wip_merge_request3,
wip_merge_request4)
end
it 'adds wip to scalar params' do

View file

@ -35,4 +35,37 @@ export default {
message: () => message,
};
},
toMatchInterpolatedText(received, match) {
let clearReceived;
let clearMatch;
try {
clearReceived = received
.replace(/\s\s+/gm, ' ')
.replace(/\s\./gm, '.')
.trim();
} catch (e) {
return { actual: received, message: 'The received value is not a string', pass: false };
}
try {
clearMatch = match.replace(/%{\w+}/gm, '').trim();
} catch (e) {
return { message: 'The comparator value is not a string', pass: false };
}
const pass = clearReceived === clearMatch;
const message = pass
? () => `
\n\n
Expected: ${this.utils.printExpected(clearReceived)}
To not equal: ${this.utils.printReceived(clearMatch)}
`
: () =>
`
\n\n
Expected: ${this.utils.printExpected(clearReceived)}
To equal: ${this.utils.printReceived(clearMatch)}
`;
return { actual: received, message, pass };
},
};

View file

@ -0,0 +1,48 @@
describe('Custom jest matchers', () => {
describe('toMatchInterpolatedText', () => {
describe('malformed input', () => {
it.each([null, 1, Symbol, Array, Object])(
'fails graciously if the expected value is %s',
expected => {
expect(expected).not.toMatchInterpolatedText('null');
},
);
});
describe('malformed matcher', () => {
it.each([null, 1, Symbol, Array, Object])(
'fails graciously if the matcher is %s',
matcher => {
expect('null').not.toMatchInterpolatedText(matcher);
},
);
});
describe('positive assertion', () => {
it.each`
htmlString | templateString
${'foo'} | ${'foo'}
${'foo'} | ${'foo%{foo}'}
${'foo '} | ${'foo'}
${'foo '} | ${'foo%{foo}'}
${'foo . '} | ${'foo%{foo}.'}
${'foo bar . '} | ${'foo%{foo} bar.'}
${'foo\n\nbar . '} | ${'foo%{foo} bar.'}
${'foo bar . .'} | ${'foo%{fooStart} bar.%{fooEnd}.'}
`('$htmlString equals $templateString', ({ htmlString, templateString }) => {
expect(htmlString).toMatchInterpolatedText(templateString);
});
});
describe('negative assertion', () => {
it.each`
htmlString | templateString
${'foo'} | ${'bar'}
${'foo'} | ${'bar%{foo}'}
${'foo'} | ${'@{lol}foo%{foo}'}
${' fo o '} | ${'foo'}
`('$htmlString does not equal $templateString', ({ htmlString, templateString }) => {
expect(htmlString).not.toMatchInterpolatedText(templateString);
});
});
});
});

View file

@ -1,22 +1,32 @@
import { shallowMount } from '@vue/test-utils';
import DetailedMetric from '~/performance_bar/components/detailed_metric.vue';
import RequestWarning from '~/performance_bar/components/request_warning.vue';
import { trimText } from 'helpers/text_helper';
describe('detailedMetric', () => {
const createComponent = props =>
shallowMount(DetailedMetric, {
let wrapper;
const createComponent = props => {
wrapper = shallowMount(DetailedMetric, {
propsData: {
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('when the current request has no details', () => {
const wrapper = createComponent({
currentRequest: {},
metric: 'gitaly',
header: 'Gitaly calls',
details: 'details',
keys: ['feature', 'request'],
beforeEach(() => {
createComponent({
currentRequest: {},
metric: 'gitaly',
header: 'Gitaly calls',
details: 'details',
keys: ['feature', 'request'],
});
});
it('does not render the element', () => {
@ -31,20 +41,22 @@ describe('detailedMetric', () => {
];
describe('with a default metric name', () => {
const wrapper = createComponent({
currentRequest: {
details: {
gitaly: {
duration: '123ms',
calls: '456',
details: requestDetails,
warnings: ['gitaly calls: 456 over 30'],
beforeEach(() => {
createComponent({
currentRequest: {
details: {
gitaly: {
duration: '123ms',
calls: '456',
details: requestDetails,
warnings: ['gitaly calls: 456 over 30'],
},
},
},
},
metric: 'gitaly',
header: 'Gitaly calls',
keys: ['feature', 'request'],
metric: 'gitaly',
header: 'Gitaly calls',
keys: ['feature', 'request'],
});
});
it('displays details', () => {
@ -87,20 +99,22 @@ describe('detailedMetric', () => {
});
describe('when using a custom metric title', () => {
const wrapper = createComponent({
currentRequest: {
details: {
gitaly: {
duration: '123ms',
calls: '456',
details: requestDetails,
beforeEach(() => {
createComponent({
currentRequest: {
details: {
gitaly: {
duration: '123ms',
calls: '456',
details: requestDetails,
},
},
},
},
metric: 'gitaly',
title: 'custom',
header: 'Gitaly calls',
keys: ['feature', 'request'],
metric: 'gitaly',
title: 'custom',
header: 'Gitaly calls',
keys: ['feature', 'request'],
});
});
it('displays the custom title', () => {
@ -108,4 +122,26 @@ describe('detailedMetric', () => {
});
});
});
describe('when the details has no duration', () => {
beforeEach(() => {
createComponent({
currentRequest: {
details: {
bullet: {
calls: '456',
details: [{ notification: 'notification', backtrace: 'backtrace' }],
},
},
},
metric: 'bullet',
header: 'Bullet notifications',
keys: ['notification'],
});
});
it('renders only the number of calls', () => {
expect(trimText(wrapper.text())).toEqual('456 notification backtrace bullet');
});
});
});

View file

@ -6,61 +6,164 @@ describe Resolvers::MergeRequestsResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:merge_request_1) { create(:merge_request, :simple, source_project: project, target_project: project) }
let_it_be(:merge_request_2) { create(:merge_request, :rebased, source_project: project, target_project: project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:common_attrs) { { author: current_user, source_project: project, target_project: project } }
let_it_be(:merge_request_1) { create(:merge_request, :simple, **common_attrs) }
let_it_be(:merge_request_2) { create(:merge_request, :rebased, **common_attrs) }
let_it_be(:merge_request_3) { create(:merge_request, :unique_branches, **common_attrs) }
let_it_be(:merge_request_4) { create(:merge_request, :unique_branches, :locked, **common_attrs) }
let_it_be(:merge_request_5) { create(:merge_request, :simple, :locked, **common_attrs) }
let_it_be(:merge_request_6) { create(:labeled_merge_request, :unique_branches, labels: create_list(:label, 2), **common_attrs) }
let_it_be(:other_project) { create(:project, :repository) }
let_it_be(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) }
let(:iid_1) { merge_request_1.iid }
let(:iid_2) { merge_request_2.iid }
let(:other_iid) { other_merge_request.iid }
before do
project.add_developer(current_user)
end
describe '#resolve' do
it 'batch-resolves by target project full path and individual IID' do
result = batch_sync(max_queries: 2) do
resolve_mr(project, iid: iid_1) + resolve_mr(project, iid: iid_2)
context 'no arguments' do
it 'returns all merge requests' do
result = resolve_mr(project, {})
expect(result).to contain_exactly(merge_request_1, merge_request_2, merge_request_3, merge_request_4, merge_request_5, merge_request_6)
end
expect(result).to contain_exactly(merge_request_1, merge_request_2)
it 'returns only merge requests that the current user can see' do
result = resolve_mr(project, {}, user: build(:user))
expect(result).to be_empty
end
end
it 'batch-resolves by target project full path and IIDS' do
result = batch_sync(max_queries: 2) do
resolve_mr(project, iids: [iid_1, iid_2])
context 'by iid alone' do
it 'batch-resolves by target project full path and individual IID' do
result = batch_sync(max_queries: 2) do
[iid_1, iid_2].map { |iid| resolve_mr_single(project, iid) }
end
expect(result).to contain_exactly(merge_request_1, merge_request_2)
end
expect(result).to contain_exactly(merge_request_1, merge_request_2)
end
it 'batch-resolves by target project full path and IIDS' do
result = batch_sync(max_queries: 2) do
resolve_mr(project, iids: [iid_1, iid_2])
end
it 'can batch-resolve merge requests from different projects' do
result = batch_sync(max_queries: 3) do
resolve_mr(project, iid: iid_1) +
resolve_mr(project, iid: iid_2) +
resolve_mr(other_project, iid: other_iid)
expect(result).to contain_exactly(merge_request_1, merge_request_2)
end
expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request)
it 'can batch-resolve merge requests from different projects' do
result = batch_sync(max_queries: 3) do
resolve_mr(project, iids: iid_1) +
resolve_mr(project, iids: iid_2) +
resolve_mr(other_project, iids: other_iid)
end
expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request)
end
it 'resolves an unknown iid to be empty' do
result = batch_sync { resolve_mr_single(project, -1) }
expect(result).to be_nil
end
it 'resolves empty iids to be empty' do
result = batch_sync { resolve_mr(project, iids: []) }
expect(result).to be_empty
end
it 'resolves an unknown project to be nil when single' do
result = batch_sync { resolve_mr_single(nil, iid_1) }
expect(result).to be_nil
end
it 'resolves an unknown project to be empty' do
result = batch_sync { resolve_mr(nil, iids: [iid_1]) }
expect(result).to be_empty
end
end
it 'resolves an unknown iid to be empty' do
result = batch_sync { resolve_mr(project, iid: -1) }
context 'by source branches' do
it 'takes one argument' do
result = resolve_mr(project, source_branch: [merge_request_3.source_branch])
expect(result.compact).to be_empty
expect(result).to contain_exactly(merge_request_3)
end
it 'takes more than one argument' do
mrs = [merge_request_3, merge_request_4]
branches = mrs.map(&:source_branch)
result = resolve_mr(project, source_branch: branches )
expect(result).to match_array(mrs)
end
end
it 'resolves empty iids to be empty' do
result = batch_sync { resolve_mr(project, iids: []) }
context 'by target branches' do
it 'takes one argument' do
result = resolve_mr(project, target_branch: [merge_request_3.target_branch])
expect(result).to be_empty
expect(result).to contain_exactly(merge_request_3)
end
it 'takes more than one argument' do
mrs = [merge_request_3, merge_request_4]
branches = mrs.map(&:target_branch)
result = resolve_mr(project, target_branch: branches )
expect(result.compact).to match_array(mrs)
end
end
it 'resolves an unknown project to be empty' do
result = batch_sync { resolve_mr(nil, iid: iid_1) }
context 'by state' do
it 'takes one argument' do
result = resolve_mr(project, state: 'locked')
expect(result.compact).to be_empty
expect(result).to contain_exactly(merge_request_4, merge_request_5)
end
end
context 'by label' do
let_it_be(:label) { merge_request_6.labels.first }
let_it_be(:with_label) { create(:labeled_merge_request, :closed, labels: [label], **common_attrs) }
it 'takes one argument' do
result = resolve_mr(project, label_name: [label.title])
expect(result).to contain_exactly(merge_request_6, with_label)
end
it 'takes multiple arguments, with semantics of ALL MUST MATCH' do
result = resolve_mr(project, label_name: merge_request_6.labels.map(&:title))
expect(result).to contain_exactly(merge_request_6)
end
end
describe 'combinations' do
it 'requires all filters' do
create(:merge_request, :closed, source_project: project, target_project: project, source_branch: merge_request_4.source_branch)
result = resolve_mr(project, source_branch: [merge_request_4.source_branch], state: 'locked')
expect(result.compact).to contain_exactly(merge_request_4)
end
end
end
def resolve_mr(project, args)
resolve(described_class, obj: project, args: args)
def resolve_mr_single(project, iid)
resolve_mr(project, { iids: iid }, resolver: described_class.single)
end
def resolve_mr(project, args, resolver: described_class, user: current_user)
resolve(resolver, obj: project, args: args, ctx: { current_user: user })
end
end

View file

@ -19,7 +19,8 @@ describe GitlabSchema.types['MergeRequest'] do
force_remove_source_branch merge_status in_progress_merge_commit_sha
merge_error allow_collaboration should_be_rebased rebase_commit_sha
rebase_in_progress merge_commit_message default_merge_commit_message
merge_ongoing source_branch_exists mergeable_discussions_state web_url
merge_ongoing mergeable_discussions_state web_url
source_branch_exists target_branch_exists
upvotes downvotes head_pipeline pipelines task_completion_status
milestone assignees participants subscribed labels discussion_locked time_estimate
total_time_spent reference

View file

@ -45,18 +45,32 @@ describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_resolver(Resolvers::IssuesResolver) }
end
describe 'merge_requests field' do
describe 'merge_request field' do
subject { described_class.fields['mergeRequest'] }
it { is_expected.to have_graphql_type(Types::MergeRequestType) }
it { is_expected.to have_graphql_resolver(Resolvers::MergeRequestsResolver.single) }
it { is_expected.to have_graphql_arguments(:iid) }
end
describe 'merge_request field' do
describe 'merge_requests field' do
subject { described_class.fields['mergeRequests'] }
it { is_expected.to have_graphql_type(Types::MergeRequestType.connection_type) }
it { is_expected.to have_graphql_resolver(Resolvers::MergeRequestsResolver) }
it do
is_expected.to have_graphql_arguments(:iids,
:source_branches,
:target_branches,
:state,
:labels,
:before,
:after,
:first,
:last
)
end
end
describe 'snippets field' do

View file

@ -553,40 +553,6 @@ describe Gitlab::UsageData, :aggregate_failures do
end
end
end
describe '#count' do
let(:relation) { double(:relation) }
it 'returns the count when counting succeeds' do
allow(relation).to receive(:count).and_return(1)
expect(described_class.count(relation, batch: false)).to eq(1)
end
it 'returns the fallback value when counting fails' do
stub_const("Gitlab::UsageData::FALLBACK", 15)
allow(relation).to receive(:count).and_raise(ActiveRecord::StatementInvalid.new(''))
expect(described_class.count(relation, batch: false)).to eq(15)
end
end
describe '#distinct_count' do
let(:relation) { double(:relation) }
it 'returns the count when counting succeeds' do
allow(relation).to receive(:distinct_count_by).and_return(1)
expect(described_class.distinct_count(relation, batch: false)).to eq(1)
end
it 'returns the fallback value when counting fails' do
stub_const("Gitlab::UsageData::FALLBACK", 15)
allow(relation).to receive(:distinct_count_by).and_raise(ActiveRecord::StatementInvalid.new(''))
expect(described_class.distinct_count(relation, batch: false)).to eq(15)
end
end
end
end
@ -605,42 +571,4 @@ describe Gitlab::UsageData, :aggregate_failures do
it_behaves_like 'usage data execution'
end
describe '#alt_usage_data' do
it 'returns the fallback when it gets an error' do
expect(described_class.alt_usage_data { raise StandardError } ).to eq(-1)
end
it 'returns the evaluated block when give' do
expect(described_class.alt_usage_data { Gitlab::CurrentSettings.uuid } ).to eq(Gitlab::CurrentSettings.uuid)
end
it 'returns the value when given' do
expect(described_class.alt_usage_data(1)).to eq 1
end
end
describe '#redis_usage_data' do
context 'with block given' do
it 'returns the fallback when it gets an error' do
expect(described_class.redis_usage_data { raise ::Redis::CommandError } ).to eq(-1)
end
it 'returns the evaluated block when given' do
expect(described_class.redis_usage_data { 1 }).to eq(1)
end
end
context 'with counter given' do
it 'returns the falback values for all counter keys when it gets an error' do
allow(::Gitlab::UsageDataCounters::WikiPageCounter).to receive(:totals).and_raise(::Redis::CommandError)
expect(described_class.redis_usage_data(::Gitlab::UsageDataCounters::WikiPageCounter)).to eql(::Gitlab::UsageDataCounters::WikiPageCounter.fallback_totals)
end
it 'returns the totals when couter is given' do
allow(::Gitlab::UsageDataCounters::WikiPageCounter).to receive(:totals).and_return({ wiki_pages_create: 2 })
expect(described_class.redis_usage_data(::Gitlab::UsageDataCounters::WikiPageCounter)).to eql({ wiki_pages_create: 2 })
end
end
end
end

View file

@ -0,0 +1,77 @@
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Utils::UsageData do
describe '#count' do
let(:relation) { double(:relation) }
it 'returns the count when counting succeeds' do
allow(relation).to receive(:count).and_return(1)
expect(described_class.count(relation, batch: false)).to eq(1)
end
it 'returns the fallback value when counting fails' do
stub_const("Gitlab::Utils::UsageData::FALLBACK", 15)
allow(relation).to receive(:count).and_raise(ActiveRecord::StatementInvalid.new(''))
expect(described_class.count(relation, batch: false)).to eq(15)
end
end
describe '#distinct_count' do
let(:relation) { double(:relation) }
it 'returns the count when counting succeeds' do
allow(relation).to receive(:distinct_count_by).and_return(1)
expect(described_class.distinct_count(relation, batch: false)).to eq(1)
end
it 'returns the fallback value when counting fails' do
stub_const("Gitlab::Utils::UsageData::FALLBACK", 15)
allow(relation).to receive(:distinct_count_by).and_raise(ActiveRecord::StatementInvalid.new(''))
expect(described_class.distinct_count(relation, batch: false)).to eq(15)
end
end
describe '#alt_usage_data' do
it 'returns the fallback when it gets an error' do
expect(described_class.alt_usage_data { raise StandardError } ).to eq(-1)
end
it 'returns the evaluated block when give' do
expect(described_class.alt_usage_data { Gitlab::CurrentSettings.uuid } ).to eq(Gitlab::CurrentSettings.uuid)
end
it 'returns the value when given' do
expect(described_class.alt_usage_data(1)).to eq 1
end
end
describe '#redis_usage_data' do
context 'with block given' do
it 'returns the fallback when it gets an error' do
expect(described_class.redis_usage_data { raise ::Redis::CommandError } ).to eq(-1)
end
it 'returns the evaluated block when given' do
expect(described_class.redis_usage_data { 1 }).to eq(1)
end
end
context 'with counter given' do
it 'returns the falback values for all counter keys when it gets an error' do
allow(::Gitlab::UsageDataCounters::WikiPageCounter).to receive(:totals).and_raise(::Redis::CommandError)
expect(described_class.redis_usage_data(::Gitlab::UsageDataCounters::WikiPageCounter)).to eql(::Gitlab::UsageDataCounters::WikiPageCounter.fallback_totals)
end
it 'returns the totals when couter is given' do
allow(::Gitlab::UsageDataCounters::WikiPageCounter).to receive(:totals).and_return({ wiki_pages_create: 2 })
expect(described_class.redis_usage_data(::Gitlab::UsageDataCounters::WikiPageCounter)).to eql({ wiki_pages_create: 2 })
end
end
end
end

View file

@ -0,0 +1,54 @@
# frozen_string_literal: true
require 'spec_helper'
describe Peek::Views::BulletDetailed do
subject { described_class.new }
before do
allow(Bullet).to receive(:enable?).and_return(bullet_enabled)
end
context 'bullet disabled' do
let(:bullet_enabled) { false }
it 'returns empty results' do
expect(subject.results).to eq({})
end
end
context 'bullet enabled' do
let(:bullet_enabled) { true }
before do
allow(Bullet).to receive_message_chain(:notification_collector, :collection).and_return(notifications)
end
context 'where there are no notifications' do
let(:notifications) { [] }
it 'returns empty results' do
expect(subject.results).to eq({})
end
end
context 'when notifications exist' do
let(:notifications) do
[
double(title: 'Title 1', body: 'Body 1', body_with_caller: "first\nsecond\n"),
double(title: 'Title 2', body: 'Body 2', body_with_caller: "first\nsecond\n")
]
end
it 'returns empty results' do
expect(subject.key).to eq('bullet')
expect(subject.results[:calls]).to eq(2)
expect(subject.results[:warnings]).to eq([Peek::Views::BulletDetailed::WARNING_MESSAGE])
expect(subject.results[:details]).to eq([
{ notification: 'Title 1: Body 1', backtrace: "first\nsecond\n" },
{ notification: 'Title 2: Body 2', backtrace: "first\nsecond\n" }
])
end
end
end
end

View file

@ -0,0 +1,174 @@
# frozen_string_literal: true
require 'spec_helper'
describe 'getting merge request listings nested in a project' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:current_user) { create(:user) }
let_it_be(:label) { create(:label) }
let_it_be(:merge_request_a) { create(:labeled_merge_request, :unique_branches, source_project: project, labels: [label]) }
let_it_be(:merge_request_b) { create(:merge_request, :closed, :unique_branches, source_project: project) }
let_it_be(:merge_request_c) { create(:labeled_merge_request, :closed, :unique_branches, source_project: project, labels: [label]) }
let_it_be(:merge_request_d) { create(:merge_request, :locked, :unique_branches, source_project: project) }
let(:results) { graphql_data.dig('project', 'mergeRequests', 'nodes') }
let(:search_params) { nil }
def query_merge_requests(fields)
graphql_query_for(
:project,
{ full_path: project.full_path },
query_graphql_field(:merge_requests, search_params, [
query_graphql_field(:nodes, nil, fields)
])
)
end
let(:query) do
query_merge_requests(all_graphql_fields_for('MergeRequest', max_depth: 1))
end
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end
# The following tests are needed to guarantee that we have correctly annotated
# all the gitaly calls. Selecting combinations of fields may mask this due to
# memoization.
context 'requesting a single field' do
let(:fresh_mr) { create(:merge_request, :unique_branches, source_project: project) }
let(:search_params) { { iids: [fresh_mr.iid.to_s] } }
before do
project.repository.expire_branches_cache
end
context 'selecting any single scalar field' do
where(:field) do
scalar_fields_of('MergeRequest').map { |name| [name] }
end
with_them do
it_behaves_like 'a working graphql query' do
let(:query) do
query_merge_requests([:iid, field].uniq)
end
before do
post_graphql(query, current_user: current_user)
end
it 'selects the correct MR' do
expect(results).to contain_exactly(a_hash_including('iid' => fresh_mr.iid.to_s))
end
end
end
end
context 'selecting any single nested field' do
where(:field, :subfield, :is_connection) do
nested_fields_of('MergeRequest').flat_map do |name, field|
type = field_type(field)
is_connection = type.name.ends_with?('Connection')
type = field_type(type.fields['nodes']) if is_connection
type.fields
.select { |_, field| !nested_fields?(field) && !required_arguments?(field) }
.map(&:first)
.map { |subfield| [name, subfield, is_connection] }
end
end
with_them do
it_behaves_like 'a working graphql query' do
let(:query) do
fld = is_connection ? query_graphql_field(:nodes, nil, [subfield]) : subfield
query_merge_requests([:iid, query_graphql_field(field, nil, [fld])])
end
before do
post_graphql(query, current_user: current_user)
end
it 'selects the correct MR' do
expect(results).to contain_exactly(a_hash_including('iid' => fresh_mr.iid.to_s))
end
end
end
end
end
shared_examples 'searching with parameters' do
let(:expected) do
mrs.map { |mr| a_hash_including('iid' => mr.iid.to_s, 'title' => mr.title) }
end
it 'finds the right mrs' do
post_graphql(query, current_user: current_user)
expect(results).to match_array(expected)
end
end
context 'there are no search params' do
let(:search_params) { nil }
let(:mrs) { [merge_request_a, merge_request_b, merge_request_c, merge_request_d] }
it_behaves_like 'searching with parameters'
end
context 'the search params do not match anything' do
let(:search_params) { { iids: %w(foo bar baz) } }
let(:mrs) { [] }
it_behaves_like 'searching with parameters'
end
context 'searching by iids' do
let(:search_params) { { iids: mrs.map(&:iid).map(&:to_s) } }
let(:mrs) { [merge_request_a, merge_request_c] }
it_behaves_like 'searching with parameters'
end
context 'searching by state' do
let(:search_params) { { state: :closed } }
let(:mrs) { [merge_request_b, merge_request_c] }
it_behaves_like 'searching with parameters'
end
context 'searching by source_branch' do
let(:search_params) { { source_branches: mrs.map(&:source_branch) } }
let(:mrs) { [merge_request_b, merge_request_c] }
it_behaves_like 'searching with parameters'
end
context 'searching by target_branch' do
let(:search_params) { { target_branches: mrs.map(&:target_branch) } }
let(:mrs) { [merge_request_a, merge_request_d] }
it_behaves_like 'searching with parameters'
end
context 'searching by label' do
let(:search_params) { { labels: [label.title] } }
let(:mrs) { [merge_request_a, merge_request_c] }
it_behaves_like 'searching with parameters'
end
context 'searching by combination' do
let(:search_params) { { state: :closed, labels: [label.title] } }
let(:mrs) { [merge_request_c] }
it_behaves_like 'searching with parameters'
end
end

View file

@ -10,7 +10,6 @@ describe API::Releases do
let(:guest) { create(:user) }
let(:non_project_member) { create(:user) }
let(:commit) { create(:commit, project: project) }
let(:last_release) { project.releases.last }
before do
project.add_maintainer(maintainer)
@ -733,109 +732,6 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:conflict)
end
end
context 'Evidence collection' do
let(:params) do
{
name: 'New release',
tag_name: 'v0.1',
description: 'Super nice release',
released_at: released_at
}.compact
end
around do |example|
Timecop.freeze { example.run }
end
subject do
post api("/projects/#{project.id}/releases", maintainer), params: params
end
context 'historical release' do
let(:released_at) { 3.weeks.ago }
it 'does not execute CreateEvidenceWorker' do
expect { subject }.not_to change(CreateEvidenceWorker.jobs, :size)
end
it 'does not create an Evidence object', :sidekiq_inline do
expect { subject }.not_to change(Releases::Evidence, :count)
end
it 'is a historical release' do
subject
expect(last_release.historical_release?).to be_truthy
end
it 'is not an upcoming release' do
subject
expect(last_release.upcoming_release?).to be_falsy
end
end
context 'immediate release' do
let(:released_at) { nil }
it 'sets `released_at` to the current dttm' do
subject
expect(last_release.updated_at).to be_like_time(Time.now)
end
it 'queues CreateEvidenceWorker' do
expect { subject }.to change(CreateEvidenceWorker.jobs, :size).by(1)
end
it 'creates Evidence', :sidekiq_inline do
expect { subject }.to change(Releases::Evidence, :count).by(1)
end
it 'is not a historical release' do
subject
expect(last_release.historical_release?).to be_falsy
end
it 'is not an upcoming release' do
subject
expect(last_release.upcoming_release?).to be_falsy
end
end
context 'upcoming release' do
let(:released_at) { 1.day.from_now }
it 'queues CreateEvidenceWorker' do
expect { subject }.to change(CreateEvidenceWorker.jobs, :size).by(1)
end
it 'queues CreateEvidenceWorker at the released_at timestamp' do
subject
expect(CreateEvidenceWorker.jobs.last['at']).to eq(released_at.to_i)
end
it 'creates Evidence', :sidekiq_inline do
expect { subject }.to change(Releases::Evidence, :count).by(1)
end
it 'is not a historical release' do
subject
expect(last_release.historical_release?).to be_falsy
end
it 'is an upcoming release' do
subject
expect(last_release.upcoming_release?).to be_truthy
end
end
end
end
describe 'PUT /projects/:id/releases/:tag_name' do

View file

@ -186,4 +186,107 @@ describe Releases::CreateService do
end
end
end
context 'Evidence collection' do
let(:params) do
{
name: 'New release',
ref: 'master',
tag: 'v0.1',
description: 'Super nice release',
released_at: released_at
}.compact
end
let(:last_release) { project.releases.last }
around do |example|
Timecop.freeze { example.run }
end
subject { service.execute }
context 'historical release' do
let(:released_at) { 3.weeks.ago }
it 'does not execute CreateEvidenceWorker' do
expect { subject }.not_to change(CreateEvidenceWorker.jobs, :size)
end
it 'does not create an Evidence object', :sidekiq_inline do
expect { subject }.not_to change(Releases::Evidence, :count)
end
it 'is a historical release' do
subject
expect(last_release.historical_release?).to be_truthy
end
it 'is not an upcoming release' do
subject
expect(last_release.upcoming_release?).to be_falsy
end
end
context 'immediate release' do
let(:released_at) { nil }
it 'sets `released_at` to the current dttm' do
subject
expect(last_release.updated_at).to be_like_time(Time.current)
end
it 'queues CreateEvidenceWorker' do
expect { subject }.to change(CreateEvidenceWorker.jobs, :size).by(1)
end
it 'creates Evidence', :sidekiq_inline do
expect { subject }.to change(Releases::Evidence, :count).by(1)
end
it 'is not a historical release' do
subject
expect(last_release.historical_release?).to be_falsy
end
it 'is not an upcoming release' do
subject
expect(last_release.upcoming_release?).to be_falsy
end
end
context 'upcoming release' do
let(:released_at) { 1.day.from_now }
it 'queues CreateEvidenceWorker' do
expect { subject }.to change(CreateEvidenceWorker.jobs, :size).by(1)
end
it 'queues CreateEvidenceWorker at the released_at timestamp' do
subject
expect(CreateEvidenceWorker.jobs.last['at'].to_i).to eq(released_at.to_i)
end
it 'creates Evidence', :sidekiq_inline do
expect { subject }.to change(Releases::Evidence, :count).by(1)
end
it 'is not a historical release' do
subject
expect(last_release.historical_release?).to be_falsy
end
it 'is an upcoming release' do
subject
expect(last_release.upcoming_release?).to be_truthy
end
end
end
end

View file

@ -304,6 +304,22 @@ module GraphqlHelpers
graphql_data.fetch(GraphqlHelpers.fieldnamerize(mutation_name))
end
def scalar_fields_of(type_name)
GitlabSchema.types[type_name].fields.map do |name, field|
next if nested_fields?(field) || required_arguments?(field)
name
end.compact
end
def nested_fields_of(type_name)
GitlabSchema.types[type_name].fields.map do |name, field|
next if !nested_fields?(field) || required_arguments?(field)
[name, field]
end.compact
end
def nested_fields?(field)
!scalar?(field) && !enum?(field)
end

View file

@ -45,11 +45,32 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests
allow_gitaly_n_plus_1 { create(:project, group: subgroup) }
end
let!(:merge_request1) { create(:merge_request, assignees: [user], author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') }
let!(:merge_request2) { create(:merge_request, :conflict, assignees: [user], author: user, source_project: project2, target_project: project1, state: 'closed') }
let!(:merge_request3) { create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') }
let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') }
let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4, title: '[WIP]') }
let!(:merge_request1) do
create(:merge_request, assignees: [user], author: user,
source_project: project2, target_project: project1,
target_branch: 'merged-target')
end
let!(:merge_request2) do
create(:merge_request, :conflict, assignees: [user], author: user,
source_project: project2, target_project: project1,
state: 'closed')
end
let!(:merge_request3) do
create(:merge_request, :simple, author: user, assignees: [user2],
source_project: project2, target_project: project2,
state: 'locked',
title: 'thing WIP thing')
end
let!(:merge_request4) do
create(:merge_request, :simple, author: user,
source_project: project3, target_project: project3,
title: 'WIP thing')
end
let_it_be(:merge_request5) do
create(:merge_request, :simple, author: user,
source_project: project4, target_project: project4,
title: '[WIP]')
end
before do
project1.add_maintainer(user)

View file

@ -22,7 +22,7 @@ RSpec.shared_examples 'resolving an issuable in GraphQL' do |type|
.with(full_path: parent.full_path)
.and_return(resolved_parent)
expect(resolver_class).to receive(:new)
expect(resolver_class.single).to receive(:new)
.with(object: resolved_parent, context: context, field: nil)
.and_call_original
@ -41,7 +41,7 @@ RSpec.shared_examples 'resolving an issuable in GraphQL' do |type|
it 'returns nil if issuable is not found' do
result = mutation.resolve_issuable(type: type, parent_path: parent.full_path, iid: "100")
result = type == :merge_request ? result.sync : result
result = result.respond_to?(:sync) ? result.sync : result
expect(result).to be_nil
end

View file

@ -0,0 +1,87 @@
# frozen_string_literal: true
require 'rake_helper'
describe 'gitlab:container_registry namespace rake tasks' do
let_it_be(:application_settings) { Gitlab::CurrentSettings }
before :all do
Rake.application.rake_require 'tasks/gitlab/container_registry'
end
describe 'configure' do
before do
stub_container_registry_config(enabled: true, api_url: 'http://registry.gitlab')
end
shared_examples 'invalid config' do
it 'does not update the application settings' do
expect { run_rake_task('gitlab:container_registry:configure') }
.to raise_error(/Registry is not enabled or registry api url is not present./)
end
end
context 'when container registry is disabled' do
before do
stub_container_registry_config(enabled: false)
end
it_behaves_like 'invalid config'
end
context 'when container registry api_url is blank' do
before do
stub_container_registry_config(api_url: '')
end
it_behaves_like 'invalid config'
end
context 'when unabled to detect the container registry type' do
it 'fails and raises an error message' do
stub_registry_info({})
run_rake_task('gitlab:container_registry:configure')
application_settings.reload
expect(application_settings.container_registry_vendor).to be_blank
expect(application_settings.container_registry_version).to be_blank
expect(application_settings.container_registry_features).to eq([])
end
end
context 'when able to detect the container registry type' do
context 'when using the GitLab container registry' do
it 'updates application settings accordingly' do
stub_registry_info(vendor: 'gitlab', version: '2.9.1-gitlab', features: %w[a,b,c])
run_rake_task('gitlab:container_registry:configure')
application_settings.reload
expect(application_settings.container_registry_vendor).to eq('gitlab')
expect(application_settings.container_registry_version).to eq('2.9.1-gitlab')
expect(application_settings.container_registry_features).to eq(%w[a,b,c])
end
end
context 'when using a third-party container registry' do
it 'updates application settings accordingly' do
stub_registry_info(vendor: 'other', version: nil, features: nil)
run_rake_task('gitlab:container_registry:configure')
application_settings.reload
expect(application_settings.container_registry_vendor).to eq('other')
expect(application_settings.container_registry_version).to be_blank
expect(application_settings.container_registry_features).to eq([])
end
end
end
end
def stub_registry_info(output)
allow_next_instance_of(ContainerRegistry::Client) do |client|
allow(client).to receive(:registry_info).and_return(output)
end
end
end