Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e7bc93852d
commit
21539fe9ab
50 changed files with 1430 additions and 468 deletions
2
Gemfile
2
Gemfile
|
@ -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'
|
||||
|
||||
|
|
|
@ -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`);
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
50
app/graphql/resolvers/concerns/resolves_merge_requests.rb
Normal file
50
app/graphql/resolvers/concerns/resolves_merge_requests.rb
Normal 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
|
18
app/graphql/resolvers/merge_request_resolver.rb
Normal file
18
app/graphql/resolvers/merge_request_resolver.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Resolve image overflow at releases list panel
|
||||
merge_request: 32307
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix creating release evidence if release is created via UI
|
||||
merge_request: 32441
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Externalize i18n strings from ./app/views/shared/_promo.html.haml
|
||||
merge_request: 32109
|
||||
author: Gilang Gumilar
|
||||
type: changed
|
5
changelogs/unreleased/ajk-gql-mr-resolvers-split.yml
Normal file
5
changelogs/unreleased/ajk-gql-mr-resolvers-split.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add filters to merge request fields
|
||||
merge_request: 32328
|
||||
author:
|
||||
type: added
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
||||
"""
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 |
|
||||
|
|
BIN
doc/development/img/bullet_v13_0.png
Normal file
BIN
doc/development/img/bullet_v13_0.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1 MiB |
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
93
lib/gitlab/utils/usage_data.rb
Normal file
93
lib/gitlab/utils/usage_data.rb
Normal 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
|
47
lib/peek/views/bullet_detailed.rb
Normal file
47
lib/peek/views/bullet_detailed.rb
Normal 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
|
24
lib/tasks/gitlab/container_registry.rake
Normal file
24
lib/tasks/gitlab/container_registry.rake
Normal 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
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
|
|
48
spec/frontend/matchers_spec.js
Normal file
48
spec/frontend/matchers_spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
77
spec/lib/gitlab/utils/usage_data_spec.rb
Normal file
77
spec/lib/gitlab/utils/usage_data_spec.rb
Normal 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
|
54
spec/lib/peek/views/bullet_detailed_spec.rb
Normal file
54
spec/lib/peek/views/bullet_detailed_spec.rb
Normal 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
|
174
spec/requests/api/graphql/project/merge_requests_spec.rb
Normal file
174
spec/requests/api/graphql/project/merge_requests_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
87
spec/tasks/gitlab/container_registry_rake_spec.rb
Normal file
87
spec/tasks/gitlab/container_registry_rake_spec.rb
Normal 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
|
Loading…
Reference in a new issue