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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-22 03:08:07 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-22 03:08:07 +0300
commit21539fe9ab9a7a9604bb667b78b08854b4976f7b (patch)
tree82fb2be49aaaace5057d1e8e208ad12a422a7bbb
parente7bc93852d0ce48c490a780b6a1adc6cc36dd342 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile2
-rw-r--r--app/assets/javascripts/frequent_items/index.js76
-rw-r--r--app/assets/javascripts/main.js3
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue11
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue5
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue2
-rw-r--r--app/graphql/mutations/concerns/mutations/resolves_issuable.rb9
-rw-r--r--app/graphql/resolvers/concerns/resolves_merge_requests.rb50
-rw-r--r--app/graphql/resolvers/merge_request_resolver.rb18
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb52
-rw-r--r--app/graphql/types/merge_request_type.rb8
-rw-r--r--app/graphql/types/permission_types/merge_request.rb12
-rw-r--r--app/services/releases/create_service.rb12
-rw-r--r--app/views/shared/_promo.html.haml6
-rw-r--r--changelogs/unreleased/212063-images-overflow-at-releases-list-panel.yml5
-rw-r--r--changelogs/unreleased/218287-release-evidence-is-not-being-collected-if-release-is-created-via-.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_promo-html-haml.yml5
-rw-r--r--changelogs/unreleased/ajk-gql-mr-resolvers-split.yml5
-rw-r--r--config/initializers/bullet.rb11
-rw-r--r--config/initializers/peek.rb1
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql35
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json106
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/development/img/bullet_v13_0.pngbin0 -> 1085958 bytes
-rw-r--r--doc/development/profiling.md10
-rw-r--r--lib/api/releases.rb11
-rw-r--r--lib/gitlab/graphql/authorize/authorize_field_service.rb13
-rw-r--r--lib/gitlab/usage_data.rb80
-rw-r--r--lib/gitlab/utils/usage_data.rb93
-rw-r--r--lib/peek/views/bullet_detailed.rb47
-rw-r--r--lib/tasks/gitlab/container_registry.rake24
-rw-r--r--locale/gitlab.pot9
-rw-r--r--spec/factories/merge_requests.rb5
-rw-r--r--spec/finders/merge_requests_finder_spec.rb77
-rw-r--r--spec/frontend/matchers.js33
-rw-r--r--spec/frontend/matchers_spec.js48
-rw-r--r--spec/frontend/performance_bar/components/detailed_metric_spec.js98
-rw-r--r--spec/graphql/resolvers/merge_requests_resolver_spec.rb157
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb3
-rw-r--r--spec/graphql/types/project_type_spec.rb18
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb72
-rw-r--r--spec/lib/gitlab/utils/usage_data_spec.rb77
-rw-r--r--spec/lib/peek/views/bullet_detailed_spec.rb54
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb174
-rw-r--r--spec/requests/api/releases_spec.rb104
-rw-r--r--spec/services/releases/create_service_spec.rb103
-rw-r--r--spec/support/helpers/graphql_helpers.rb16
-rw-r--r--spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb31
-rw-r--r--spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb4
-rw-r--r--spec/tasks/gitlab/container_registry_rake_spec.rb87
50 files changed, 1425 insertions, 463 deletions
diff --git a/Gemfile b/Gemfile
index 9c8c5e8b30d..2a018f3e0ee 100644
--- a/Gemfile
+++ b/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'
diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js
index 6263acbab8e..c074f173776 100644
--- a/app/assets/javascripts/frequent_items/index.js
+++ b/app/assets/javascripts/frequent_items/index.js
@@ -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`);
+ }),
+ );
+ });
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 713f57a2b27..dbe445b374d 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -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);
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index e1a0e2df0e0..ef24dbfb6ce 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -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">
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 1df5562e1b6..41147ccaea8 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -38,6 +38,11 @@ export default {
keys: ['sql'],
},
{
+ metric: 'bullet',
+ header: s__('PerformanceBar|Bullet notifications'),
+ keys: ['notification'],
+ },
+ {
metric: 'gitaly',
header: s__('PerformanceBar|Gitaly calls'),
keys: ['feature', 'request'],
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
index 58045b57d80..adb0e69b786 100644
--- a/app/assets/javascripts/releases/components/release_block.vue
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -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>
diff --git a/app/graphql/mutations/concerns/mutations/resolves_issuable.rb b/app/graphql/mutations/concerns/mutations/resolves_issuable.rb
index d63cc27a450..f3ed3565b03 100644
--- a/app/graphql/mutations/concerns/mutations/resolves_issuable.rb
+++ b/app/graphql/mutations/concerns/mutations/resolves_issuable.rb
@@ -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
diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
new file mode 100644
index 00000000000..779ff0b50d4
--- /dev/null
+++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
@@ -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
diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb
new file mode 100644
index 00000000000..a47a128ea32
--- /dev/null
+++ b/app/graphql/resolvers/merge_request_resolver.rb
@@ -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
diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb
index 25121dce005..44fc4e17cd4 100644
--- a/app/graphql/resolvers/merge_requests_resolver.rb
+++ b/app/graphql/resolvers/merge_requests_resolver.rb
@@ -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
-
- alias_method :project, :object
+ 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.'
- def resolve(**args)
- project = object.respond_to?(:sync) ? object.sync : object
- return MergeRequest.none if project.nil?
+ 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.'
- args[:iids] ||= [args[:iid]].compact
+ argument :state, ::Types::MergeRequestStateEnum,
+ required: false,
+ description: 'A merge request state. If provided, all resolved merge requests will have this state.'
- if args[:iids].any?
- batch_load_merge_requests(args[:iids])
- else
- args[:project_id] = project.id
+ 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.'
- 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
- 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
+ def no_results_possible?(args)
+ project.nil? || args.values.any? { |v| v.is_a?(Array) && v.empty? }
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index cd4c6b4d46a..6ac385a8e31 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -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,
diff --git a/app/graphql/types/permission_types/merge_request.rb b/app/graphql/types/permission_types/merge_request.rb
index d877fc177d2..28b7ebd2af6 100644
--- a/app/graphql/types/permission_types/merge_request.rb
+++ b/app/graphql/types/permission_types/merge_request.rb
@@ -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
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index 81ca9d6d123..2a4cbe2dc4b 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -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
diff --git a/app/views/shared/_promo.html.haml b/app/views/shared/_promo.html.haml
index 0f31b60d8d3..855f6b9c1f4 100644
--- a/app/views/shared/_promo.html.haml
+++ b/app/views/shared/_promo.html.haml
@@ -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'
diff --git a/changelogs/unreleased/212063-images-overflow-at-releases-list-panel.yml b/changelogs/unreleased/212063-images-overflow-at-releases-list-panel.yml
new file mode 100644
index 00000000000..dcfe1e56491
--- /dev/null
+++ b/changelogs/unreleased/212063-images-overflow-at-releases-list-panel.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve image overflow at releases list panel
+merge_request: 32307
+author:
+type: fixed
diff --git a/changelogs/unreleased/218287-release-evidence-is-not-being-collected-if-release-is-created-via-.yml b/changelogs/unreleased/218287-release-evidence-is-not-being-collected-if-release-is-created-via-.yml
new file mode 100644
index 00000000000..101b8c60287
--- /dev/null
+++ b/changelogs/unreleased/218287-release-evidence-is-not-being-collected-if-release-is-created-via-.yml
@@ -0,0 +1,5 @@
+---
+title: Fix creating release evidence if release is created via UI
+merge_request: 32441
+author:
+type: fixed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_promo-html-haml.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_promo-html-haml.yml
new file mode 100644
index 00000000000..fb2b87754da
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_promo-html-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/_promo.html.haml
+merge_request: 32109
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/ajk-gql-mr-resolvers-split.yml b/changelogs/unreleased/ajk-gql-mr-resolvers-split.yml
new file mode 100644
index 00000000000..02c84076fc3
--- /dev/null
+++ b/changelogs/unreleased/ajk-gql-mr-resolvers-split.yml
@@ -0,0 +1,5 @@
+---
+title: Add filters to merge request fields
+merge_request: 32328
+author:
+type: added
diff --git a/config/initializers/bullet.rb b/config/initializers/bullet.rb
index 0ade7109420..d1f72ca3ce7 100644
--- a/config/initializers/bullet.rb
+++ b/config/initializers/bullet.rb
@@ -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
diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb
index a3810be70b2..9aa5cb61d75 100644
--- a/config/initializers/peek.rb
+++ b/config/initializers/peek.rb
@@ -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?
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index e3b11d4cd1f..48ef6f5ae36 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -6006,6 +6006,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
"""
targetProject: Project!
@@ -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
"""
@@ -7899,19 +7899,34 @@ type Project {
first: Int
"""
- IID of the merge request, for example `1`
+ Array of IIDs of merge requests, for example `[1, 2]`
"""
- iid: String
+ iids: [String!]
"""
- Array of IIDs of merge requests, for example `[1, 2]`
+ Array of label names. All resolved merge requests will have all of these labels.
"""
- iids: [String!]
+ 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
"""
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 746d7120ed4..923830512ff 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -16718,6 +16718,24 @@
"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",
"args": [
@@ -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,
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index a6e06925b2d..1fe468c54c3 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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 |
diff --git a/doc/development/img/bullet_v13_0.png b/doc/development/img/bullet_v13_0.png
new file mode 100644
index 00000000000..04b476db581
--- /dev/null
+++ b/doc/development/img/bullet_v13_0.png
Binary files differ
diff --git a/doc/development/profiling.md b/doc/development/profiling.md
index 2589329fc83..2cab6750b9b 100644
--- a/doc/development/profiling.md
+++ b/doc/development/profiling.md
@@ -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
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index 95b3e90323c..a5bb1a44f1f 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -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
diff --git a/lib/gitlab/graphql/authorize/authorize_field_service.rb b/lib/gitlab/graphql/authorize/authorize_field_service.rb
index 61668b634fd..0af621b6741 100644
--- a/lib/gitlab/graphql/authorize/authorize_field_service.rb
+++ b/lib/gitlab/graphql/authorize/authorize_field_service.rb
@@ -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
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 0193752a30e..7aeb06035b1 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -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
diff --git a/lib/gitlab/utils/usage_data.rb b/lib/gitlab/utils/usage_data.rb
new file mode 100644
index 00000000000..a7fe36a689d
--- /dev/null
+++ b/lib/gitlab/utils/usage_data.rb
@@ -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
diff --git a/lib/peek/views/bullet_detailed.rb b/lib/peek/views/bullet_detailed.rb
new file mode 100644
index 00000000000..8e6f72f565e
--- /dev/null
+++ b/lib/peek/views/bullet_detailed.rb
@@ -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
diff --git a/lib/tasks/gitlab/container_registry.rake b/lib/tasks/gitlab/container_registry.rake
new file mode 100644
index 00000000000..d167851dc6b
--- /dev/null
+++ b/lib/tasks/gitlab/container_registry.rake
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index fb812d9695c..96d1606176c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index b10c04a37f7..be4c79621ff 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -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(
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index b6f2c7bb992..7eb3d8b67a9 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -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 }
+ 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
- merge_requests = described_class.new(user, params).execute
+ [: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 }
- expect(merge_requests).to contain_exactly(merge_request1)
+ it_behaves_like 'scalar or array parameter'
+ end
end
- it 'filters by source branch' do
- params = { source_branch: merge_request2.source_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)
- end
+ it 'accepts a single label' do
+ found = find(distinct_labels.first.title)
+ common = find(common_labels.first.title)
- it 'filters by target branch' do
- params = { target_branch: merge_request2.target_branch }
+ expect(found).to contain_exactly(merge_requests.first)
+ expect(common).to match_array(merge_requests)
+ end
- merge_requests = described_class.new(user, params).execute
+ 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(merge_requests).to contain_exactly(merge_request2)
+ 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
diff --git a/spec/frontend/matchers.js b/spec/frontend/matchers.js
index 35c362d0bf5..53c6a72eea0 100644
--- a/spec/frontend/matchers.js
+++ b/spec/frontend/matchers.js
@@ -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 };
+ },
};
diff --git a/spec/frontend/matchers_spec.js b/spec/frontend/matchers_spec.js
new file mode 100644
index 00000000000..0a2478f978a
--- /dev/null
+++ b/spec/frontend/matchers_spec.js
@@ -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);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/performance_bar/components/detailed_metric_spec.js b/spec/frontend/performance_bar/components/detailed_metric_spec.js
index 01b6b7b043c..f040dcfdea4 100644
--- a/spec/frontend/performance_bar/components/detailed_metric_spec.js
+++ b/spec/frontend/performance_bar/components/detailed_metric_spec.js
@@ -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,25 +99,49 @@ describe('detailedMetric', () => {
});
describe('when using a custom metric title', () => {
- const wrapper = createComponent({
+ beforeEach(() => {
+ createComponent({
+ currentRequest: {
+ details: {
+ gitaly: {
+ duration: '123ms',
+ calls: '456',
+ details: requestDetails,
+ },
+ },
+ },
+ metric: 'gitaly',
+ title: 'custom',
+ header: 'Gitaly calls',
+ keys: ['feature', 'request'],
+ });
+ });
+
+ it('displays the custom title', () => {
+ expect(wrapper.text()).toContain('custom');
+ });
+ });
+ });
+
+ describe('when the details has no duration', () => {
+ beforeEach(() => {
+ createComponent({
currentRequest: {
details: {
- gitaly: {
- duration: '123ms',
+ bullet: {
calls: '456',
- details: requestDetails,
+ details: [{ notification: 'notification', backtrace: 'backtrace' }],
},
},
},
- metric: 'gitaly',
- title: 'custom',
- header: 'Gitaly calls',
- keys: ['feature', 'request'],
+ metric: 'bullet',
+ header: 'Bullet notifications',
+ keys: ['notification'],
});
+ });
- it('displays the custom title', () => {
- expect(wrapper.text()).toContain('custom');
- });
+ it('renders only the number of calls', () => {
+ expect(trimText(wrapper.text())).toEqual('456 notification backtrace bullet');
});
});
});
diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
index 4217d257ab3..6ff7e1ecac6 100644
--- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb
+++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
@@ -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
+
+ 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
+
+ expect(result).to contain_exactly(merge_request_1, merge_request_2)
+ end
+
+ 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
- expect(result).to contain_exactly(merge_request_1, merge_request_2)
+ 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 '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)
+ context 'by source branches' do
+ it 'takes one argument' do
+ result = resolve_mr(project, source_branch: [merge_request_3.source_branch])
+
+ expect(result).to contain_exactly(merge_request_3)
end
- expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request)
+ 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 an unknown iid to be empty' do
- result = batch_sync { resolve_mr(project, iid: -1) }
+ context 'by target branches' do
+ it 'takes one argument' do
+ result = resolve_mr(project, target_branch: [merge_request_3.target_branch])
+
+ expect(result).to contain_exactly(merge_request_3)
+ end
- expect(result.compact).to be_empty
+ 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 empty iids to be empty' do
- result = batch_sync { resolve_mr(project, iids: []) }
+ context 'by state' do
+ it 'takes one argument' do
+ result = resolve_mr(project, state: 'locked')
- expect(result).to be_empty
+ expect(result).to contain_exactly(merge_request_4, merge_request_5)
+ end
end
- it 'resolves an unknown project to be empty' do
- result = batch_sync { resolve_mr(nil, iid: iid_1) }
+ 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) }
- expect(result.compact).to be_empty
+ 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
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index e7ab2100084..ec8ea01bcea 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -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
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 6368f743720..f0eb027534e 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 7ff4570d3e3..0ade188af77 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb
new file mode 100644
index 00000000000..7f53cc27a21
--- /dev/null
+++ b/spec/lib/gitlab/utils/usage_data_spec.rb
@@ -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
diff --git a/spec/lib/peek/views/bullet_detailed_spec.rb b/spec/lib/peek/views/bullet_detailed_spec.rb
new file mode 100644
index 00000000000..a482cadc7db
--- /dev/null
+++ b/spec/lib/peek/views/bullet_detailed_spec.rb
@@ -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
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
new file mode 100644
index 00000000000..8e5876af29e
--- /dev/null
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -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
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index 237782a681c..f4cb7f25990 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -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
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index d0859500440..ece145dcc4b 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -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
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 7082424b899..57305512406 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -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
diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
index 617701abf27..2b8daa80ab4 100644
--- a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
+++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
@@ -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)
diff --git a/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb b/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb
index b56181371c3..1befc459023 100644
--- a/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb
@@ -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
diff --git a/spec/tasks/gitlab/container_registry_rake_spec.rb b/spec/tasks/gitlab/container_registry_rake_spec.rb
new file mode 100644
index 00000000000..216f914c5d0
--- /dev/null
+++ b/spec/tasks/gitlab/container_registry_rake_spec.rb
@@ -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