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 }} - {{ sprintf(__('%{duration}ms'), { duration: item.duration }) }} + {{ + sprintf(__('%{duration}ms'), { duration: item.duration }) + }}
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 @@ -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'), 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 {
-
+
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 + 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 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 @@ -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 """ 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 @@ -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, 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 Binary files /dev/null and b/doc/development/img/bullet_v13_0.png 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 } - - 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 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,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'); + }); + }); }); 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 - 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 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