diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-28 21:08:35 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-28 21:08:35 +0300 |
commit | 6315ed9630fb1c6ade3114beb762cd1568d79219 (patch) | |
tree | 2a5d31936d09c14420c8f4c8bd752e268f0eb19f | |
parent | fedf978f9aa1909ed7bb3fad767ad120a1c6bd7b (diff) |
Add latest changes from gitlab-org/gitlab@master
57 files changed, 1276 insertions, 162 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index 076de55014e..b1699ce147f 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -1,15 +1,19 @@ # Make sure to update all the similar conditions in other CI config files if you modify these conditions -.if-default: &if-default - if: '$CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/ || $CI_COMMIT_REF_NAME =~ /^\d+-\d+-auto-deploy-\d+$/ || $CI_COMMIT_REF_NAME =~ /^security\// || $CI_MERGE_REQUEST_IID || $CI_COMMIT_TAG' +.if-not-canonical-namespace: &if-not-canonical-namespace + if: '$CI_PROJECT_NAMESPACE !~ /^gitlab(-org)?($|\/)/' # Make sure to update all the similar conditions in other CI config files if you modify these conditions -.if-default-ee: &if-default-ee - if: '($CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/ || $CI_COMMIT_REF_NAME =~ /^\d+-\d+-auto-deploy-\d+$/ || $CI_COMMIT_REF_NAME =~ /^security\// || $CI_MERGE_REQUEST_IID || $CI_COMMIT_TAG) && $CI_PROJECT_NAME =~ /^gitlab(-ee)?$/' +.if-not-ee: &if-not-ee + if: '$CI_PROJECT_NAME !~ /^gitlab(-ee)?$/' # Make sure to update all the similar conditions in other CI config files if you modify these conditions -.if-master: &if-master +.if-master-refs: &if-master-refs if: '$CI_COMMIT_REF_NAME == "master"' +# Make sure to update all the similar conditions in other CI config files if you modify these conditions +.if-default-refs: &if-default-refs + if: '$CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/ || $CI_COMMIT_REF_NAME =~ /^\d+-\d+-auto-deploy-\d+$/ || $CI_COMMIT_REF_NAME =~ /^security\// || $CI_MERGE_REQUEST_IID || $CI_COMMIT_TAG' + # Make sure to update all the similar patterns in other CI config files if you modify these patterns .code-backstage-patterns: &code-backstage-patterns - ".gitlab/ci/**/*" @@ -33,6 +37,32 @@ - "{,ee/}spec/**/*" - "doc/README.md" # Some RSpec test rely on this file +# Make sure to update all the similar patterns in other CI config files if you modify these patterns +.code-backstage-patterns-qa: &code-backstage-patterns-qa + - ".gitlab/ci/**/*" + - ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" + - ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml" + - ".csscomb.json" + - "Dockerfile.assets" + - "*_VERSION" + - "Gemfile{,.lock}" + - "Rakefile" + - "{babel.config,jest.config}.js" + - "config.ru" + - "{package.json,yarn.lock}" + - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*" + - "doc/api/graphql/reference/*" # Files in this folder are auto-generated + # Backstage changes + - "Dangerfile" + - "danger/**/*" + - "{,ee/}fixtures/**/*" + - "{,ee/}rubocop/**/*" + - "{,ee/}spec/**/*" + - "doc/README.md" # Some RSpec test rely on this file + # QA changes + - ".dockerignore" + - "qa/**/*" + .assets-compile-cache: cache: paths: @@ -46,10 +76,8 @@ extends: - .default-tags - .default-retry - - .default-only - .default-before_script - .assets-compile-cache - - .only:changes-code-backstage-qa image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-19.03.1 stage: prepare services: @@ -80,24 +108,29 @@ - time scripts/build_assets_image - scripts/clean-old-cached-assets - rm -f /etc/apt/sources.list.d/google*.list # We don't need to update Chrome here - only: - variables: - - $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org($|\/)/ # Matches the gitlab-org group and its subgroups - - $CI_SERVER_HOST == "dev.gitlab.org" tags: - gitlab-org - docker gitlab:assets:compile pull-push-cache: extends: .gitlab:assets:compile-metadata - only: - refs: - - master + rules: + - <<: *if-not-canonical-namespace + when: never + - <<: *if-master-refs + changes: *code-backstage-patterns-qa + when: on_success cache: policy: pull-push gitlab:assets:compile pull-cache: extends: .gitlab:assets:compile-metadata + rules: + - <<: *if-not-canonical-namespace + when: never + - <<: *if-default-refs + changes: *code-backstage-patterns-qa + when: on_success cache: policy: pull @@ -105,10 +138,8 @@ gitlab:assets:compile pull-cache: extends: - .default-tags - .default-retry - - .default-only - .default-before_script - .assets-compile-cache - - .only:changes-code-backstage-qa stage: prepare script: - node --version @@ -130,28 +161,46 @@ gitlab:assets:compile pull-cache: compile-assets pull-push-cache: extends: .compile-assets-metadata - only: - refs: - - master + rules: + - <<: *if-master-refs + changes: *code-backstage-patterns-qa + when: on_success cache: policy: pull-push compile-assets pull-push-cache foss: - extends: [".compile-assets-metadata", ".only-ee-as-if-foss"] - only: - refs: - - master + extends: + - .compile-assets-metadata + - .as-if-foss + rules: + - <<: *if-not-ee + when: never + - <<: *if-master-refs + changes: *code-backstage-patterns-qa + when: on_success cache: policy: pull-push key: "assets-compile:v8:foss" compile-assets pull-cache: extends: .compile-assets-metadata + rules: + - <<: *if-default-refs + changes: *code-backstage-patterns-qa + when: on_success cache: policy: pull compile-assets pull-cache foss: - extends: [".compile-assets-metadata", ".only-ee-as-if-foss"] + extends: + - .compile-assets-metadata + - .as-if-foss + rules: + - <<: *if-not-ee + when: never + - <<: *if-default-refs + changes: *code-backstage-patterns-qa + when: on_success cache: policy: pull key: "assets-compile:v8:foss" @@ -240,7 +289,7 @@ jest-foss: - .default-cache stage: test rules: - - <<: *if-master + - <<: *if-master-refs when: on_success dependencies: [] cache: @@ -274,7 +323,7 @@ webpack-dev-server: - .default-cache stage: test rules: - - <<: *if-default + - <<: *if-default-refs changes: *code-backstage-patterns when: on_success needs: ["setup-test-env", "compile-assets pull-cache"] diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 6943f51047c..d6fcddee6e8 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -230,7 +230,11 @@ - $CI_PROJECT_NAME == "gitlab" - $CI_PROJECT_NAME == "gitlab-ee" # Support former project name for forks/mirrors -.only-ee-as-if-foss: - extends: .only-ee +.as-if-foss: variables: FOSS_ONLY: '1' + +.only-ee-as-if-foss: + extends: + - .only-ee + - .as-if-foss diff --git a/app/assets/javascripts/broadcast_notification.js b/app/assets/javascripts/broadcast_notification.js new file mode 100644 index 00000000000..b124502506a --- /dev/null +++ b/app/assets/javascripts/broadcast_notification.js @@ -0,0 +1,21 @@ +import Cookies from 'js-cookie'; + +const handleOnDismiss = ({ currentTarget }) => { + currentTarget.removeEventListener('click', handleOnDismiss); + const { + dataset: { id }, + } = currentTarget; + + Cookies.set(`hide_broadcast_notification_message_${id}`, true); + + const notification = document.querySelector(`.js-broadcast-notification-${id}`); + notification.parentNode.removeChild(notification); +}; + +export default () => { + const dismissButton = document.querySelector('.js-dismiss-current-broadcast-notification'); + + if (dismissButton) { + dismissButton.addEventListener('click', handleOnDismiss); + } +}; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index d755e7e8cdb..1e07469bd7a 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -35,6 +35,7 @@ import initPerformanceBar from './performance_bar'; import initSearchAutocomplete from './search_autocomplete'; import GlFieldErrors from './gl_field_errors'; import initUserPopovers from './user_popovers'; +import initBroadcastNotifications from './broadcast_notification'; import { initUserTracking } from './tracking'; import { __ } from './locale'; @@ -105,6 +106,7 @@ function deferredInitialisation() { initUsagePingConsent(); initUserPopovers(); initUserTracking(); + initBroadcastNotifications(); if (document.querySelector('.search')) initSearchAutocomplete(); diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index b95fd8800c0..34e65c322c6 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -6,19 +6,16 @@ module BroadcastMessagesHelper end def current_broadcast_notification_message - BroadcastMessage.current_notification_messages(request.path).last + not_hidden_messages = BroadcastMessage.current_notification_messages(request.path).select do |message| + cookies["hide_broadcast_notification_message_#{message.id}"].blank? + end + not_hidden_messages.last end def broadcast_message(message, opts = {}) return unless message.present? - classes = "broadcast-#{message.broadcast_type}-message #{opts[:preview] && 'preview'}" - - content_tag :div, dir: 'auto', class: classes, style: broadcast_message_style(message) do - concat sprite_icon('bullhorn', size: 16, css_class: 'vertical-align-text-top') - concat ' ' - concat render_broadcast_message(message) - end + render "shared/broadcast_message", { message: message, opts: opts } end def broadcast_message_style(broadcast_message) diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 9eca324f0fc..4b205cbe67a 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -27,7 +27,8 @@ module Ci license_management: 'gl-license-management-report.json', license_scanning: 'gl-license-scanning-report.json', performance: 'performance.json', - metrics: 'metrics.txt' + metrics: 'metrics.txt', + lsif: 'lsif.sqlite3' }.freeze INTERNAL_TYPES = { @@ -52,7 +53,8 @@ module Ci dast: :raw, license_management: :raw, license_scanning: :raw, - performance: :raw + performance: :raw, + lsif: :raw }.freeze TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze @@ -114,7 +116,8 @@ module Ci performance: 11, ## EE-specific metrics: 12, ## EE-specific metrics_referee: 13, ## runner referees - network_referee: 14 ## runner referees + network_referee: 14, ## runner referees + lsif: 15 # LSIF dump for code navigation } enum file_format: { diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb index 3e9b084e784..4a632e8cd0c 100644 --- a/app/models/concerns/atomic_internal_id.rb +++ b/app/models/concerns/atomic_internal_id.rb @@ -27,7 +27,7 @@ module AtomicInternalId extend ActiveSupport::Concern class_methods do - def has_internal_id(column, scope:, init:, ensure_if: nil, track_if: nil, presence: true) # rubocop:disable Naming/PredicateName + def has_internal_id(column, scope:, init:, ensure_if: nil, track_if: nil, presence: true, backfill: false) # rubocop:disable Naming/PredicateName # We require init here to retain the ability to recalculate in the absence of a # InternalId record (we may delete records in `internal_ids` for example). raise "has_internal_id requires a init block, none given." unless init @@ -38,6 +38,8 @@ module AtomicInternalId validates column, presence: presence define_method("ensure_#{scope}_#{column}!") do + return if backfill && self.class.where(column => nil).exists? + scope_value = internal_id_read_scope(scope) value = read_attribute(column) return value unless scope_value diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index 8d3eeaf2461..3e8d0c6a778 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -21,7 +21,7 @@ class InternalId < ApplicationRecord belongs_to :project belongs_to :namespace - enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5 } + enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5, operations_feature_flags: 6 } validates :usage, presence: true diff --git a/app/models/performance_monitoring/prometheus_dashboard.rb b/app/models/performance_monitoring/prometheus_dashboard.rb new file mode 100644 index 00000000000..5f2df444fd0 --- /dev/null +++ b/app/models/performance_monitoring/prometheus_dashboard.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module PerformanceMonitoring + class PrometheusDashboard + include ActiveModel::Model + + attr_accessor :dashboard, :panel_groups + + validates :dashboard, presence: true + validates :panel_groups, presence: true + + def self.from_json(json_content) + dashboard = new( + dashboard: json_content['dashboard'], + panel_groups: json_content['panel_groups'].map { |group| PrometheusPanelGroup.from_json(group) } + ) + + dashboard.tap(&:validate!) + end + + def to_yaml + self.as_json(only: valid_attributes).to_yaml + end + + private + + def valid_attributes + %w(panel_groups panels metrics group priority type title y_label weight id unit label query query_range dashboard) + end + end +end diff --git a/app/models/performance_monitoring/prometheus_metric.rb b/app/models/performance_monitoring/prometheus_metric.rb new file mode 100644 index 00000000000..7b8bef906fa --- /dev/null +++ b/app/models/performance_monitoring/prometheus_metric.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module PerformanceMonitoring + class PrometheusMetric + include ActiveModel::Model + + attr_accessor :id, :unit, :label, :query, :query_range + + validates :unit, presence: true + validates :query, presence: true, unless: :query_range + validates :query_range, presence: true, unless: :query + + def self.from_json(json_content) + metric = PrometheusMetric.new( + id: json_content['id'], + unit: json_content['unit'], + label: json_content['label'], + query: json_content['query'], + query_range: json_content['query_range'] + ) + + metric.tap(&:validate!) + end + end +end diff --git a/app/models/performance_monitoring/prometheus_panel.rb b/app/models/performance_monitoring/prometheus_panel.rb new file mode 100644 index 00000000000..c03218b4219 --- /dev/null +++ b/app/models/performance_monitoring/prometheus_panel.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module PerformanceMonitoring + class PrometheusPanel + include ActiveModel::Model + + attr_accessor :type, :title, :y_label, :weight, :metrics + + validates :title, presence: true + validates :metrics, presence: true + + def self.from_json(json_content) + panel = new( + type: json_content['type'], + title: json_content['title'], + y_label: json_content['y_label'], + weight: json_content['weight'], + metrics: json_content['metrics'].map { |metric| PrometheusMetric.from_json(metric) } + ) + + panel.tap(&:validate!) + end + end +end diff --git a/app/models/performance_monitoring/prometheus_panel_group.rb b/app/models/performance_monitoring/prometheus_panel_group.rb new file mode 100644 index 00000000000..e672545fce3 --- /dev/null +++ b/app/models/performance_monitoring/prometheus_panel_group.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module PerformanceMonitoring + class PrometheusPanelGroup + include ActiveModel::Model + + attr_accessor :group, :priority, :panels + + validates :group, presence: true + validates :panels, presence: true + + def self.from_json(json_content) + panel_group = new( + group: json_content['group'], + priority: json_content['priority'], + panels: json_content['panels'].map { |panel| PrometheusPanel.from_json(panel) } + ) + + panel_group.tap(&:validate!) + end + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index c0d1750fe42..ee919f844dc 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -927,22 +927,12 @@ class Repository def ancestor?(ancestor_id, descendant_id) return false if ancestor_id.nil? || descendant_id.nil? - counter = Gitlab::Metrics.counter( - :repository_ancestor_calls_total, - 'The number of times we call Repository#ancestor with valid arguments') - cache_hit = true - cache_key = "ancestor:#{ancestor_id}:#{descendant_id}" - result = request_store_cache.fetch(cache_key) do + request_store_cache.fetch(cache_key) do cache.fetch(cache_key) do - cache_hit = false raw_repository.ancestor?(ancestor_id, descendant_id) end end - - counter.increment(cache_hit: cache_hit.to_s) - - result end def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil, prune: true) diff --git a/app/views/shared/_broadcast_message.html.haml b/app/views/shared/_broadcast_message.html.haml new file mode 100644 index 00000000000..c058b210688 --- /dev/null +++ b/app/views/shared/_broadcast_message.html.haml @@ -0,0 +1,8 @@ +%div{ class: "broadcast-#{message.broadcast_type}-message #{opts[:preview] && 'preview'} js-broadcast-notification-#{message.id} d-flex", + style: broadcast_message_style(message), dir: 'auto' } + %div + = sprite_icon('bullhorn', size: 16, css_class: 'vertical-align-text-top') + = render_broadcast_message(message) + - if message.notification? && opts[:preview].blank? + %button.js-dismiss-current-broadcast-notification.btn.btn-link.text-dark.pl-2.pr-2{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id } } + %i.fa.fa-times diff --git a/changelogs/unreleased/21801-migrate-epic-and-epic-notes-mentions-to-epic-user-mentions-table.yml b/changelogs/unreleased/21801-migrate-epic-and-epic-notes-mentions-to-epic-user-mentions-table.yml new file mode 100644 index 00000000000..168a8c7ac12 --- /dev/null +++ b/changelogs/unreleased/21801-migrate-epic-and-epic-notes-mentions-to-epic-user-mentions-table.yml @@ -0,0 +1,5 @@ +--- +title: Migrate epic, epic notes mentions to respective DB table +merge_request: 22333 +author: +type: changed diff --git a/changelogs/unreleased/custom-dashboard-validation.yml b/changelogs/unreleased/custom-dashboard-validation.yml new file mode 100644 index 00000000000..cd425e6dcf7 --- /dev/null +++ b/changelogs/unreleased/custom-dashboard-validation.yml @@ -0,0 +1,5 @@ +--- +title: Add validation for custom PrometheusDashboard +merge_request: 22893 +author: +type: added diff --git a/changelogs/unreleased/id-upload-lsif-dump.yml b/changelogs/unreleased/id-upload-lsif-dump.yml new file mode 100644 index 00000000000..62ba8f650ca --- /dev/null +++ b/changelogs/unreleased/id-upload-lsif-dump.yml @@ -0,0 +1,5 @@ +--- +title: Add support for lsif artifact report +merge_request: 23672 +author: +type: added diff --git a/changelogs/unreleased/ops-ff-iid.yml b/changelogs/unreleased/ops-ff-iid.yml new file mode 100644 index 00000000000..8c17eb23520 --- /dev/null +++ b/changelogs/unreleased/ops-ff-iid.yml @@ -0,0 +1,5 @@ +--- +title: Add iid to operations_feature_flags and backfill +merge_request: 22175 +author: +type: added diff --git a/changelogs/unreleased/sh-fix-empty-namespace-git-access.yml b/changelogs/unreleased/sh-fix-empty-namespace-git-access.yml new file mode 100644 index 00000000000..a44a7d36e7f --- /dev/null +++ b/changelogs/unreleased/sh-fix-empty-namespace-git-access.yml @@ -0,0 +1,5 @@ +--- +title: Eliminate statement timeouts when namespace is blank +merge_request: 23839 +author: +type: fixed diff --git a/config/routes.rb b/config/routes.rb index 518cf985718..be0ef0106f4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -120,9 +120,7 @@ Rails.application.routes.draw do draw :country draw :country_state draw :subscription - end - Gitlab.ee do constraints(-> (*) { Gitlab::Analytics.any_features_enabled? }) do draw :analytics end @@ -168,11 +166,6 @@ Rails.application.routes.draw do end end - draw :api - draw :sidekiq - draw :help - draw :snippets - # Invites resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do member do @@ -193,6 +186,25 @@ Rails.application.routes.draw do # Notification settings resources :notification_settings, only: [:create, :update] + resources :groups, only: [:index, :new, :create] do + post :preview_markdown + end + + resources :projects, only: [:index, :new, :create] + + get '/projects/:id' => 'projects#resolve' + + Gitlab.ee do + scope '/-/push_from_secondary/:geo_node_id' do + draw :git_http + end + end + + draw :git_http + draw :api + draw :sidekiq + draw :help + draw :snippets draw :google_api draw :import draw :uploads diff --git a/config/routes/group.rb b/config/routes/group.rb index 24957d5ecef..8f572a685d8 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -1,11 +1,5 @@ # frozen_string_literal: true -# rubocop: disable Cop/PutGroupRoutesUnderScope -resources :groups, only: [:index, :new, :create] do - post :preview_markdown -end -# rubocop: enable Cop/PutGroupRoutesUnderScope - constraints(::Constraints::GroupUrlConstrainer.new) do scope(path: 'groups/*id', controller: :groups, diff --git a/config/routes/project.rb b/config/routes/project.rb index df505f96c9d..e1950724386 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -1,13 +1,5 @@ # frozen_string_literal: true -# rubocop: disable Cop/PutProjectRoutesUnderScope -resources :projects, only: [:index, :new, :create] - -draw :git_http - -get '/projects/:id' => 'projects#resolve' -# rubocop: enable Cop/PutProjectRoutesUnderScope - constraints(::Constraints::ProjectUrlConstrainer.new) do # If the route has a wildcard segment, the segment has a regex constraint, # the segment is potentially followed by _another_ wildcard segment, and diff --git a/db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb b/db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb index e6427534310..55ef3c79f3f 100644 --- a/db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb +++ b/db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb @@ -7,9 +7,13 @@ class AddMergeTrainEnabledToCiCdSettings < ActiveRecord::Migration[5.1] disable_ddl_transaction! + # rubocop:disable Migration/UpdateLargeTable + # rubocop:disable Migration/AddColumnWithDefault def up - add_column_with_default :project_ci_cd_settings, :merge_trains_enabled, :boolean, default: false, allow_null: false # rubocop:disable Migration/AddColumnWithDefault + add_column_with_default :project_ci_cd_settings, :merge_trains_enabled, :boolean, default: false, allow_null: false end + # rubocop:enable Migration/UpdateLargeTable + # rubocop:enable Migration/AddColumnWithDefault def down remove_column :project_ci_cd_settings, :merge_trains_enabled diff --git a/db/migrate/20200117194830_add_iid_to_operations_feature_flags.rb b/db/migrate/20200117194830_add_iid_to_operations_feature_flags.rb new file mode 100644 index 00000000000..b18d9788b9f --- /dev/null +++ b/db/migrate/20200117194830_add_iid_to_operations_feature_flags.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddIidToOperationsFeatureFlags < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def up + add_column :operations_feature_flags, :iid, :integer + end + + def down + remove_column :operations_feature_flags, :iid + end +end diff --git a/db/migrate/20200117194840_add_index_on_operations_feature_flags_iid.rb b/db/migrate/20200117194840_add_index_on_operations_feature_flags_iid.rb new file mode 100644 index 00000000000..67dedb56ec1 --- /dev/null +++ b/db/migrate/20200117194840_add_index_on_operations_feature_flags_iid.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexOnOperationsFeatureFlagsIid < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :operations_feature_flags, [:project_id, :iid], unique: true + end + + def down + remove_concurrent_index :operations_feature_flags, [:project_id, :iid] + end +end diff --git a/db/post_migrate/20191115115043_migrate_epic_mentions_to_db.rb b/db/post_migrate/20191115115043_migrate_epic_mentions_to_db.rb new file mode 100644 index 00000000000..97f2e568a7e --- /dev/null +++ b/db/post_migrate/20191115115043_migrate_epic_mentions_to_db.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class MigrateEpicMentionsToDb < ActiveRecord::Migration[5.2] + DOWNTIME = false + + disable_ddl_transaction! + + DELAY = 2.minutes.to_i + BATCH_SIZE = 10000 + MIGRATION = 'UserMentions::CreateResourceUserMention' + + JOIN = "LEFT JOIN epic_user_mentions on epics.id = epic_user_mentions.epic_id" + QUERY_CONDITIONS = "(description like '%@%' OR title like '%@%') AND epic_user_mentions.epic_id is null" + + class Epic < ActiveRecord::Base + include EachBatch + + self.table_name = 'epics' + end + + def up + return unless Gitlab.ee? + + Epic + .joins(JOIN) + .where(QUERY_CONDITIONS) + .each_batch(of: BATCH_SIZE) do |batch, index| + range = batch.pluck(Arel.sql('MIN(epics.id)'), Arel.sql('MAX(epics.id)')).first + BackgroundMigrationWorker.perform_in(index * DELAY, MIGRATION, ['Epic', JOIN, QUERY_CONDITIONS, false, *range]) + end + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20191115115522_migrate_epic_notes_mentions_to_db.rb b/db/post_migrate/20191115115522_migrate_epic_notes_mentions_to_db.rb new file mode 100644 index 00000000000..e0b3c36b57d --- /dev/null +++ b/db/post_migrate/20191115115522_migrate_epic_notes_mentions_to_db.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class MigrateEpicNotesMentionsToDb < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + DELAY = 2.minutes.to_i + BATCH_SIZE = 10000 + MIGRATION = 'UserMentions::CreateResourceUserMention' + + INDEX_NAME = 'epic_mentions_temp_index' + INDEX_CONDITION = "note LIKE '%@%'::text AND notes.noteable_type = 'Epic'" + QUERY_CONDITIONS = "#{INDEX_CONDITION} AND epic_user_mentions.epic_id IS NULL" + JOIN = 'LEFT JOIN epic_user_mentions ON notes.id = epic_user_mentions.note_id' + + class Note < ActiveRecord::Base + include EachBatch + + self.table_name = 'notes' + end + + def up + return unless Gitlab.ee? + + # create temporary index for notes with mentions, may take well over 1h + add_concurrent_index(:notes, :id, where: INDEX_CONDITION, name: INDEX_NAME) + + Note + .joins(JOIN) + .where(QUERY_CONDITIONS) + .each_batch(of: BATCH_SIZE) do |batch, index| + range = batch.pluck(Arel.sql('MIN(notes.id)'), Arel.sql('MAX(notes.id)')).first + BackgroundMigrationWorker.perform_in(index * DELAY, MIGRATION, ['Epic', JOIN, QUERY_CONDITIONS, true, *range]) + end + end + + def down + # no-op + # temporary index is to be dropped in a different migration in an upcoming release: + # https://gitlab.com/gitlab-org/gitlab/issues/196842 + end +end diff --git a/db/post_migrate/20191128162854_drop_project_ci_cd_settings_merge_trains_enabled.rb b/db/post_migrate/20191128162854_drop_project_ci_cd_settings_merge_trains_enabled.rb index df5c6c8f6cc..c2e6792e611 100644 --- a/db/post_migrate/20191128162854_drop_project_ci_cd_settings_merge_trains_enabled.rb +++ b/db/post_migrate/20191128162854_drop_project_ci_cd_settings_merge_trains_enabled.rb @@ -12,6 +12,6 @@ class DropProjectCiCdSettingsMergeTrainsEnabled < ActiveRecord::Migration[5.2] end def down - add_column_with_default :project_ci_cd_settings, :merge_trains_enabled, :boolean, default: false, allow_null: true + add_column_with_default :project_ci_cd_settings, :merge_trains_enabled, :boolean, default: false, allow_null: true # rubocop:disable Migration/UpdateLargeTable end end diff --git a/db/post_migrate/20200117194850_backfill_operations_feature_flags_iid.rb b/db/post_migrate/20200117194850_backfill_operations_feature_flags_iid.rb new file mode 100644 index 00000000000..bc97bd6062d --- /dev/null +++ b/db/post_migrate/20200117194850_backfill_operations_feature_flags_iid.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class BackfillOperationsFeatureFlagsIid < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + ### + # This should update about 700 rows on gitlab.com + # Execution time is predicted to take less than a second based on #database-lab results + # https://gitlab.com/gitlab-org/gitlab/merge_requests/22175#migration-performance + ### + def up + execute('LOCK operations_feature_flags IN ACCESS EXCLUSIVE MODE') + + backfill_iids('operations_feature_flags') + + change_column_null :operations_feature_flags, :iid, false + end + + def down + change_column_null :operations_feature_flags, :iid, true + end +end diff --git a/db/post_migrate/20200117194900_delete_internal_ids_where_feature_flags_usage.rb b/db/post_migrate/20200117194900_delete_internal_ids_where_feature_flags_usage.rb new file mode 100644 index 00000000000..0cf1ab03622 --- /dev/null +++ b/db/post_migrate/20200117194900_delete_internal_ids_where_feature_flags_usage.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class DeleteInternalIdsWhereFeatureFlagsUsage < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + sql = <<~SQL + DELETE FROM internal_ids WHERE usage = 6 + SQL + + execute(sql) + end + + def down + # no-op + end +end diff --git a/db/schema.rb b/db/schema.rb index e4ec6e55fa6..f48ead215bc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -2778,6 +2778,7 @@ ActiveRecord::Schema.define(version: 2020_01_27_090233) do t.index ["commit_id"], name: "index_notes_on_commit_id" t.index ["created_at"], name: "index_notes_on_created_at" t.index ["discussion_id"], name: "index_notes_on_discussion_id" + t.index ["id"], name: "epic_mentions_temp_index", where: "((note ~~ '%@%'::text) AND ((noteable_type)::text = 'Epic'::text))" t.index ["line_code"], name: "index_notes_on_line_code" t.index ["note"], name: "index_notes_on_note_trigram", opclass: :gin_trgm_ops, using: :gin t.index ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type" @@ -2881,6 +2882,8 @@ ActiveRecord::Schema.define(version: 2020_01_27_090233) do t.datetime_with_timezone "updated_at", null: false t.string "name", null: false t.text "description" + t.integer "iid", null: false + t.index ["project_id", "iid"], name: "index_operations_feature_flags_on_project_id_and_iid", unique: true t.index ["project_id", "name"], name: "index_operations_feature_flags_on_project_id_and_name", unique: true end diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md index 2c506898aeb..09747595fc0 100644 --- a/doc/administration/high_availability/README.md +++ b/doc/administration/high_availability/README.md @@ -327,7 +327,7 @@ On different cloud vendors a best effort like for like can be used. | Service | Nodes | Configuration | GCP type | | ----------------------------|-------|-----------------------|---------------| | GitLab Rails[^1] | 15 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | -| PostgreSQL | 3 | 8 vCPU, 30GB Memory | n1-standard-8 | +| PostgreSQL | 3 | 16 vCPU, 60GB Memory | n1-standard-16 | | PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | | Gitaly[^2] [^7] | X | 64 vCPU, 240GB Memory | n1-standard-64 | | Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | diff --git a/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb b/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb new file mode 100644 index 00000000000..e951b44b036 --- /dev/null +++ b/lib/gitlab/background_migration/user_mentions/create_resource_user_mention.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + module UserMentions + class CreateResourceUserMention + # Resources that have mentions to be migrated: + # issue, merge_request, epic, commit, snippet, design + + BULK_INSERT_SIZE = 5000 + ISOLATION_MODULE = 'Gitlab::BackgroundMigration::UserMentions::Models' + + def perform(resource_model, join, conditions, with_notes, start_id, end_id) + resource_model = "#{ISOLATION_MODULE}::#{resource_model}".constantize if resource_model.is_a?(String) + model = with_notes ? "#{ISOLATION_MODULE}::Note".constantize : resource_model + resource_user_mention_model = resource_model.user_mention_model + + records = model.joins(join).where(conditions).where(id: start_id..end_id) + + records.in_groups_of(BULK_INSERT_SIZE, false).each do |records| + mentions = [] + records.each do |record| + mentions << record.build_mention_values + end + + no_quote_columns = [:note_id] + no_quote_columns << resource_user_mention_model.resource_foreign_key + + Gitlab::Database.bulk_insert( + resource_user_mention_model.table_name, + mentions, + return_ids: true, + disable_quote: no_quote_columns, + on_conflict: :do_nothing + ) + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/user_mentions/models/epic.rb b/lib/gitlab/background_migration/user_mentions/models/epic.rb new file mode 100644 index 00000000000..019d8f0ea8b --- /dev/null +++ b/lib/gitlab/background_migration/user_mentions/models/epic.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + module UserMentions + module Models + class Epic < ActiveRecord::Base + include IsolatedMentionable + include CacheMarkdownField + + attr_mentionable :title, pipeline: :single_line + attr_mentionable :description + cache_markdown_field :title, pipeline: :single_line + cache_markdown_field :description, issuable_state_filter_enabled: true + + self.table_name = 'epics' + + belongs_to :author, class_name: "User" + belongs_to :project + belongs_to :group + + def self.user_mention_model + Gitlab::BackgroundMigration::UserMentions::Models::EpicUserMention + end + + def user_mention_model + self.class.user_mention_model + end + + def project + nil + end + + def mentionable_params + { group: group, label_url_method: :group_epics_url } + end + + def user_mention_resource_id + id + end + + def user_mention_note_id + 'NULL' + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/user_mentions/models/epic_user_mention.rb b/lib/gitlab/background_migration/user_mentions/models/epic_user_mention.rb new file mode 100644 index 00000000000..4e3ce9bf3a7 --- /dev/null +++ b/lib/gitlab/background_migration/user_mentions/models/epic_user_mention.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + module UserMentions + module Models + class EpicUserMention < ActiveRecord::Base + self.table_name = 'epic_user_mentions' + + def self.resource_foreign_key + :epic_id + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/user_mentions/models/isolated_mentionable.rb b/lib/gitlab/background_migration/user_mentions/models/isolated_mentionable.rb new file mode 100644 index 00000000000..40aab896212 --- /dev/null +++ b/lib/gitlab/background_migration/user_mentions/models/isolated_mentionable.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + module UserMentions + module Models + # == IsolatedMentionable concern + # + # Shortcutted for isolation version of Mentionable to be used in mentions migrations + # + module IsolatedMentionable + extend ::ActiveSupport::Concern + + class_methods do + # Indicate which attributes of the Mentionable to search for GFM references. + def attr_mentionable(attr, options = {}) + attr = attr.to_s + mentionable_attrs << [attr, options] + end + end + + included do + # Accessor for attributes marked mentionable. + cattr_accessor :mentionable_attrs, instance_accessor: false do + [] + end + + if self < Participable + participant -> (user, ext) { all_references(user, extractor: ext) } + end + end + + def all_references(current_user = nil, extractor: nil) + # Use custom extractor if it's passed in the function parameters. + if extractor + extractors[current_user] = extractor + else + extractor = extractors[current_user] ||= ::Gitlab::ReferenceExtractor.new(project, current_user) + + extractor.reset_memoized_values + end + + self.class.mentionable_attrs.each do |attr, options| + text = __send__(attr) # rubocop:disable GitlabSecurity/PublicSend + options = options.merge( + cache_key: [self, attr], + author: author, + skip_project_check: skip_project_check? + ).merge(mentionable_params) + + cached_html = self.try(:updated_cached_html_for, attr.to_sym) + options[:rendered] = cached_html if cached_html + + extractor.analyze(text, options) + end + + extractor + end + + def extractors + @extractors ||= {} + end + + def skip_project_check? + false + end + + def build_mention_values + refs = all_references(author) + + { + "#{self.user_mention_model.resource_foreign_key}": user_mention_resource_id, + note_id: user_mention_note_id, + mentioned_users_ids: array_to_sql(refs.mentioned_users.pluck(:id)), + mentioned_projects_ids: array_to_sql(refs.mentioned_projects.pluck(:id)), + mentioned_groups_ids: array_to_sql(refs.mentioned_groups.pluck(:id)) + } + end + + def array_to_sql(ids_array) + return unless ids_array.present? + + '{' + ids_array.join(", ") + '}' + end + + private + + def mentionable_params + {} + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/user_mentions/models/note.rb b/lib/gitlab/background_migration/user_mentions/models/note.rb new file mode 100644 index 00000000000..c2828202907 --- /dev/null +++ b/lib/gitlab/background_migration/user_mentions/models/note.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + module UserMentions + module Models + class Note < ActiveRecord::Base + include IsolatedMentionable + include CacheMarkdownField + + self.table_name = 'notes' + self.inheritance_column = :_type_disabled + + attr_mentionable :note, pipeline: :note + cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true + + belongs_to :author, class_name: "User" + belongs_to :noteable, polymorphic: true + belongs_to :project + + def user_mention_model + "#{CreateResourceUserMention::ISOLATION_MODULE}::#{noteable.class}".constantize.user_mention_model + end + + def for_personal_snippet? + noteable.class.name == 'PersonalSnippet' + end + + def for_project_noteable? + !for_personal_snippet? + end + + def skip_project_check? + !for_project_noteable? + end + + def for_epic? + noteable.class.name == 'Epic' + end + + def user_mention_resource_id + noteable_id || commit_id + end + + def user_mention_note_id + id + end + + private + + def mentionable_params + return super unless for_epic? + + super.merge(banzai_context_params) + end + + def banzai_context_params + { group: noteable.group, label_url_method: :group_epics_url } + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb index f984d7d397a..571e056e096 100644 --- a/lib/gitlab/ci/config/entry/reports.rb +++ b/lib/gitlab/ci/config/entry/reports.rb @@ -11,7 +11,7 @@ module Gitlab include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Attributable - ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management license_scanning metrics].freeze + ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management license_scanning metrics lsif].freeze attributes ALLOWED_KEYS @@ -30,6 +30,7 @@ module Gitlab validates :license_management, array_of_strings_or_string: true validates :license_scanning, array_of_strings_or_string: true validates :metrics, array_of_strings_or_string: true + validates :lsif, array_of_strings_or_string: true end end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 5077143e15e..b53e01e6ff2 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -1119,6 +1119,20 @@ into similar problems in the future (e.g. when new tables are created). SQL end + # Note this should only be used with very small tables + def backfill_iids(table) + sql = <<-END + UPDATE #{table} + SET iid = #{table}_with_calculated_iid.iid_num + FROM ( + SELECT id, ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY id ASC) AS iid_num FROM #{table} + ) AS #{table}_with_calculated_iid + WHERE #{table}.id = #{table}_with_calculated_iid.id + END + + execute(sql) + end + private def tables_match?(target_table, foreign_key_table) diff --git a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb index d27da186de0..d126fdb2be2 100644 --- a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb +++ b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb @@ -47,11 +47,7 @@ module Gitlab private def cache - @cache ||= if Feature.enabled?(:hset_redis_diff_caching, project, default_enabled: true) - Gitlab::Diff::HighlightCache.new(self) - else - Gitlab::Diff::DeprecatedHighlightCache.new(self) - end + @cache ||= Gitlab::Diff::HighlightCache.new(self) end end end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 7e9ec097ef7..906350e57c5 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -50,8 +50,8 @@ module Gitlab @project = project @protocol = protocol @authentication_abilities = authentication_abilities - @namespace_path = namespace_path - @project_path = project_path + @namespace_path = namespace_path || project&.namespace&.full_path + @project_path = project_path || project&.path @redirected_path = redirected_path @auth_result_type = auth_result_type end @@ -60,6 +60,7 @@ module Gitlab @logger = Checks::TimedLogger.new(timeout: INTERNAL_TIMEOUT, header: LOG_HEADER) @changes = changes + check_namespace! check_protocol! check_valid_actor! check_active_user! @@ -136,6 +137,12 @@ module Gitlab end end + def check_namespace! + return if namespace_path.present? + + raise NotFoundError, ERROR_MESSAGES[:project_not_found] + end + def check_active_user! return unless user diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb index 70303a30153..dea85f68be2 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true module QA - context 'Manage', :orchestrated, :oauth, quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/196517' do + # This test is skipped instead of quarantine because continuously running + # this test may cause the user to hit GitHub's rate limits thus blocking the user. + # Related issue: https://gitlab.com/gitlab-org/gitlab/issues/196517 + context 'Manage', :orchestrated, :oauth, :skip do describe 'OAuth login' do it 'User logs in to GitLab with GitHub OAuth' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/rubocop/cop/migration/update_large_table.rb b/rubocop/cop/migration/update_large_table.rb index c15eec22d04..94bba31c249 100644 --- a/rubocop/cop/migration/update_large_table.rb +++ b/rubocop/cop/migration/update_large_table.rb @@ -34,6 +34,7 @@ module RuboCop namespaces notes projects + project_ci_cd_settings routes users ].freeze diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 7cdb8989692..e2ffc595c44 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1129,8 +1129,9 @@ describe Projects::IssuesController do sign_in(user) end - it "rejects a developer to destroy an issue" do + it "does not delete the issue, returning :not_found" do delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: issue.iid } + expect(response).to have_gitlab_http_status(:not_found) end end diff --git a/spec/features/broadcast_messages_spec.rb b/spec/features/broadcast_messages_spec.rb new file mode 100644 index 00000000000..43fbf1010c9 --- /dev/null +++ b/spec/features/broadcast_messages_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Broadcast Messages' do + let!(:broadcast_message) { create(:broadcast_message, broadcast_type: 'notification', message: 'SampleMessage') } + + it 'shows broadcast message' do + visit root_path + + expect(page).to have_content 'SampleMessage' + end + + it 'hides broadcast message after dismiss', :js do + visit root_path + + find('.js-dismiss-current-broadcast-notification').click + + expect(page).not_to have_content 'SampleMessage' + end + + it 'broadcast message is still hidden after refresh', :js do + visit root_path + + find('.js-dismiss-current-broadcast-notification').click + visit root_path + + expect(page).not_to have_content 'SampleMessage' + end +end diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb index a0682c0e278..7e181e429d7 100644 --- a/spec/helpers/broadcast_messages_helper_spec.rb +++ b/spec/helpers/broadcast_messages_helper_spec.rb @@ -3,6 +3,29 @@ require 'spec_helper' describe BroadcastMessagesHelper do + describe 'current_broadcast_notification_message' do + subject { helper.current_broadcast_notification_message } + + context 'with available broadcast notification messages' do + let!(:broadcast_message_1) { create(:broadcast_message, broadcast_type: 'notification', starts_at: Time.now - 1.day) } + let!(:broadcast_message_2) { create(:broadcast_message, broadcast_type: 'notification', starts_at: Time.now) } + + it { is_expected.to eq broadcast_message_2 } + + context 'when last broadcast message is hidden' do + before do + helper.request.cookies["hide_broadcast_notification_message_#{broadcast_message_2.id}"] = 'true' + end + + it { is_expected.to eq broadcast_message_1 } + end + end + + context 'without broadcast notification messages' do + it { is_expected.to be_nil } + end + end + describe 'broadcast_message' do let(:current_broadcast_message) { BroadcastMessage.new(message: 'Current Message') } diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb index 8562885c90c..1ec30976284 100644 --- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -43,6 +43,7 @@ describe Gitlab::Ci::Config::Entry::Reports do :license_management | 'gl-license-management-report.json' :license_scanning | 'gl-license-scanning-report.json' :performance | 'performance.json' + :lsif | 'lsif.sqlite3' end with_them do diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index f71d3a67eb9..3797e794985 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -1531,4 +1531,366 @@ describe Gitlab::Database::MigrationHelpers do expect(buffer.read).to include("\"class\":\"#{model.class}\"") end end + + describe '#backfill_iids' do + include MigrationsHelpers + + class self::Issue < ActiveRecord::Base + include AtomicInternalId + + self.table_name = 'issues' + self.inheritance_column = :_type_disabled + + belongs_to :project + + has_internal_id :iid, + scope: :project, + init: ->(s) { s&.project&.issues&.maximum(:iid) }, + backfill: true, + presence: false + end + + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + + def setup + namespace = namespaces.create!(name: 'foo', path: 'foo') + project = projects.create!(namespace_id: namespace.id) + + project + end + + it 'generates iids properly for models created after the migration' do + project = setup + + model.backfill_iids('issues') + + issue = self.class::Issue.create!(project_id: project.id) + + expect(issue.iid).to eq(1) + end + + it 'generates iids properly for models created after the migration when iids are backfilled' do + project = setup + issue_a = issues.create!(project_id: project.id) + + model.backfill_iids('issues') + + issue_b = self.class::Issue.create!(project_id: project.id) + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.iid).to eq(2) + end + + it 'generates iids properly for models created after the migration across multiple projects' do + project_a = setup + project_b = setup + issues.create!(project_id: project_a.id) + issues.create!(project_id: project_b.id) + issues.create!(project_id: project_b.id) + + model.backfill_iids('issues') + + issue_a = self.class::Issue.create!(project_id: project_a.id) + issue_b = self.class::Issue.create!(project_id: project_b.id) + + expect(issue_a.iid).to eq(2) + expect(issue_b.iid).to eq(3) + end + + context 'when the new code creates a row post deploy but before the migration runs' do + it 'does not change the row iid' do + project = setup + issue = self.class::Issue.create!(project_id: project.id) + + model.backfill_iids('issues') + + expect(issue.reload.iid).to eq(1) + end + + it 'backfills iids for rows already in the database' do + project = setup + issue_a = issues.create!(project_id: project.id) + issue_b = issues.create!(project_id: project.id) + issue_c = self.class::Issue.create!(project_id: project.id) + + model.backfill_iids('issues') + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(2) + expect(issue_c.reload.iid).to eq(3) + end + + it 'backfills iids across multiple projects' do + project_a = setup + project_b = setup + issue_a = issues.create!(project_id: project_a.id) + issue_b = issues.create!(project_id: project_b.id) + issue_c = self.class::Issue.create!(project_id: project_a.id) + issue_d = self.class::Issue.create!(project_id: project_b.id) + + model.backfill_iids('issues') + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(1) + expect(issue_c.reload.iid).to eq(2) + expect(issue_d.reload.iid).to eq(2) + end + + it 'generates iids properly for models created after the migration' do + project = setup + issue_a = issues.create!(project_id: project.id) + issue_b = issues.create!(project_id: project.id) + issue_c = self.class::Issue.create!(project_id: project.id) + + model.backfill_iids('issues') + + issue_d = self.class::Issue.create!(project_id: project.id) + issue_e = self.class::Issue.create!(project_id: project.id) + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(2) + expect(issue_c.reload.iid).to eq(3) + expect(issue_d.iid).to eq(4) + expect(issue_e.iid).to eq(5) + end + + it 'backfills iids and properly generates iids for new models across multiple projects' do + project_a = setup + project_b = setup + issue_a = issues.create!(project_id: project_a.id) + issue_b = issues.create!(project_id: project_b.id) + issue_c = self.class::Issue.create!(project_id: project_a.id) + issue_d = self.class::Issue.create!(project_id: project_b.id) + + model.backfill_iids('issues') + + issue_e = self.class::Issue.create!(project_id: project_a.id) + issue_f = self.class::Issue.create!(project_id: project_b.id) + issue_g = self.class::Issue.create!(project_id: project_a.id) + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(1) + expect(issue_c.reload.iid).to eq(2) + expect(issue_d.reload.iid).to eq(2) + expect(issue_e.iid).to eq(3) + expect(issue_f.iid).to eq(3) + expect(issue_g.iid).to eq(4) + end + end + + context 'when the new code creates a model and then old code creates a model post deploy but before the migration runs' do + it 'backfills iids' do + project = setup + issue_a = issues.create!(project_id: project.id) + issue_b = self.class::Issue.create!(project_id: project.id) + issue_c = issues.create!(project_id: project.id) + + model.backfill_iids('issues') + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(2) + expect(issue_c.reload.iid).to eq(3) + end + + it 'generates an iid for a new model after the migration' do + project = setup + issue_a = issues.create!(project_id: project.id) + issue_b = issues.create!(project_id: project.id) + issue_c = self.class::Issue.create!(project_id: project.id) + issue_d = issues.create!(project_id: project.id) + + model.backfill_iids('issues') + + issue_e = self.class::Issue.create!(project_id: project.id) + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(2) + expect(issue_c.reload.iid).to eq(3) + expect(issue_d.reload.iid).to eq(4) + expect(issue_e.iid).to eq(5) + end + end + + context 'when the new code and old code alternate creating models post deploy but before the migration runs' do + it 'backfills iids' do + project = setup + issue_a = issues.create!(project_id: project.id) + issue_b = self.class::Issue.create!(project_id: project.id) + issue_c = issues.create!(project_id: project.id) + issue_d = self.class::Issue.create!(project_id: project.id) + + model.backfill_iids('issues') + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(2) + expect(issue_c.reload.iid).to eq(3) + expect(issue_d.reload.iid).to eq(4) + end + + it 'generates an iid for a new model after the migration' do + project = setup + issue_a = issues.create!(project_id: project.id) + issue_b = issues.create!(project_id: project.id) + issue_c = self.class::Issue.create!(project_id: project.id) + issue_d = issues.create!(project_id: project.id) + issue_e = self.class::Issue.create!(project_id: project.id) + + model.backfill_iids('issues') + + issue_f = self.class::Issue.create!(project_id: project.id) + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(2) + expect(issue_c.reload.iid).to eq(3) + expect(issue_d.reload.iid).to eq(4) + expect(issue_e.reload.iid).to eq(5) + expect(issue_f.iid).to eq(6) + end + end + + context 'when the new code creates and deletes a model post deploy but before the migration runs' do + it 'backfills iids for rows already in the database' do + project = setup + issue_a = issues.create!(project_id: project.id) + issue_b = issues.create!(project_id: project.id) + issue_c = self.class::Issue.create!(project_id: project.id) + issue_c.delete + + model.backfill_iids('issues') + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(2) + end + + it 'successfully creates a new model after the migration' do + project = setup + issue_a = issues.create!(project_id: project.id) + issue_b = issues.create!(project_id: project.id) + issue_c = self.class::Issue.create!(project_id: project.id) + issue_c.delete + + model.backfill_iids('issues') + + issue_d = self.class::Issue.create!(project_id: project.id) + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(2) + expect(issue_d.iid).to eq(3) + end + end + + context 'when the new code creates and deletes a model and old code creates a model post deploy but before the migration runs' do + it 'backfills iids' do + project = setup + issue_a = issues.create!(project_id: project.id) + issue_b = issues.create!(project_id: project.id) + issue_c = self.class::Issue.create!(project_id: project.id) + issue_c.delete + issue_d = issues.create!(project_id: project.id) + + model.backfill_iids('issues') + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(2) + expect(issue_d.reload.iid).to eq(3) + end + + it 'successfully creates a new model after the migration' do + project = setup + issue_a = issues.create!(project_id: project.id) + issue_b = issues.create!(project_id: project.id) + issue_c = self.class::Issue.create!(project_id: project.id) + issue_c.delete + issue_d = issues.create!(project_id: project.id) + + model.backfill_iids('issues') + + issue_e = self.class::Issue.create!(project_id: project.id) + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(2) + expect(issue_d.reload.iid).to eq(3) + expect(issue_e.iid).to eq(4) + end + end + + context 'when the new code creates and deletes a model and then creates another model post deploy but before the migration runs' do + it 'successfully generates an iid for a new model after the migration' do + project = setup + issue_a = issues.create!(project_id: project.id) + issue_b = issues.create!(project_id: project.id) + issue_c = self.class::Issue.create!(project_id: project.id) + issue_c.delete + issue_d = self.class::Issue.create!(project_id: project.id) + + model.backfill_iids('issues') + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(2) + expect(issue_d.reload.iid).to eq(3) + end + + it 'successfully generates an iid for a new model after the migration' do + project = setup + issue_a = issues.create!(project_id: project.id) + issue_b = issues.create!(project_id: project.id) + issue_c = self.class::Issue.create!(project_id: project.id) + issue_c.delete + issue_d = self.class::Issue.create!(project_id: project.id) + + model.backfill_iids('issues') + + issue_e = self.class::Issue.create!(project_id: project.id) + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(2) + expect(issue_d.reload.iid).to eq(3) + expect(issue_e.iid).to eq(4) + end + end + + context 'when the first model is created for a project after the migration' do + it 'generates an iid' do + project_a = setup + project_b = setup + issue_a = issues.create!(project_id: project_a.id) + + model.backfill_iids('issues') + + issue_b = self.class::Issue.create!(project_id: project_b.id) + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(1) + end + end + + context 'when a row already has an iid set in the database' do + it 'backfills iids' do + project = setup + issue_a = issues.create!(project_id: project.id, iid: 1) + issue_b = issues.create!(project_id: project.id, iid: 2) + + model.backfill_iids('issues') + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(2) + end + + it 'backfills for multiple projects' do + project_a = setup + project_b = setup + issue_a = issues.create!(project_id: project_a.id, iid: 1) + issue_b = issues.create!(project_id: project_b.id, iid: 1) + issue_c = issues.create!(project_id: project_a.id, iid: 2) + + model.backfill_iids('issues') + + expect(issue_a.reload.iid).to eq(1) + expect(issue_b.reload.iid).to eq(1) + expect(issue_c.reload.iid).to eq(2) + end + end + end end diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb index a4f74ddc8c2..c2b6ca4164c 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -38,21 +38,6 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do let(:diffable) { merge_request.merge_request_diff } end - context 'using Gitlab::Diff::DeprecatedHighlightCache' do - before do - stub_feature_flags(hset_redis_diff_caching: false) - end - - it 'uses a different cache key if diff line keys change' do - mr_diff = described_class.new(merge_request.merge_request_diff, diff_options: nil) - key = mr_diff.cache_key - - stub_const('Gitlab::Diff::Line::SERIALIZE_KEYS', [:foo]) - - expect(mr_diff.cache_key).not_to eq(key) - end - end - it_behaves_like 'diff statistics' do let(:collection_default_args) do { diff_options: {} } diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 36bde9de12d..231bcb4150c 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -75,6 +75,32 @@ describe Gitlab::GitAccess do end end + describe '#check_namespace!' do + context 'when namespace exists' do + before do + project.add_maintainer(user) + end + + it 'allows push and pull access' do + aggregate_failures do + expect { push_access_check }.not_to raise_error + expect { pull_access_check }.not_to raise_error + end + end + end + + context 'when namespace does not exist' do + let(:namespace_path) { nil } + + it 'does not allow push and pull access' do + aggregate_failures do + expect { push_access_check }.to raise_not_found + expect { pull_access_check }.to raise_not_found + end + end + end + end + describe '#check_project_accessibility!' do context 'when the project exists' do context 'when actor exists' do diff --git a/spec/migrations/backfill_operations_feature_flags_iid_spec.rb b/spec/migrations/backfill_operations_feature_flags_iid_spec.rb new file mode 100644 index 00000000000..f7a223e794a --- /dev/null +++ b/spec/migrations/backfill_operations_feature_flags_iid_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20200117194850_backfill_operations_feature_flags_iid.rb') + +describe BackfillOperationsFeatureFlagsIid, :migration do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:flags) { table(:operations_feature_flags) } + + def setup + namespace = namespaces.create!(name: 'foo', path: 'foo') + project = projects.create!(namespace_id: namespace.id) + + project + end + + it 'migrates successfully when there are no flags in the database' do + setup + + disable_migrations_output { migrate! } + + expect(flags.count).to eq(0) + end + + it 'migrates successfully with a row in the table in both FOSS and EE' do + project = setup + flags.create!(project_id: project.id, active: true, name: 'test_flag') + + disable_migrations_output { migrate! } + + expect(flags.count).to eq(1) + end +end diff --git a/spec/migrations/delete_internal_ids_where_feature_flags_usage_spec.rb b/spec/migrations/delete_internal_ids_where_feature_flags_usage_spec.rb new file mode 100644 index 00000000000..b9c6b489aca --- /dev/null +++ b/spec/migrations/delete_internal_ids_where_feature_flags_usage_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20200117194900_delete_internal_ids_where_feature_flags_usage') + +describe DeleteInternalIdsWhereFeatureFlagsUsage, :migration do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:internal_ids) { table(:internal_ids) } + + def setup + namespace = namespaces.create!(name: 'foo', path: 'foo') + project = projects.create!(namespace_id: namespace.id) + + project + end + + it 'deletes feature flag rows from the internal_ids table' do + project = setup + internal_ids.create!(project_id: project.id, usage: 6, last_value: 1) + + disable_migrations_output { migrate! } + + expect(internal_ids.count).to eq(0) + end + + it 'does not delete issue rows from the internal_ids table' do + project = setup + internal_ids.create!(project_id: project.id, usage: 0, last_value: 1) + + disable_migrations_output { migrate! } + + expect(internal_ids.count).to eq(1) + end + + it 'does not delete merge request rows from the internal_ids table' do + project = setup + internal_ids.create!(project_id: project.id, usage: 1, last_value: 1) + + disable_migrations_output { migrate! } + + expect(internal_ids.count).to eq(1) + end +end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 845c906129f..19a45ce5f88 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2450,15 +2450,6 @@ describe Repository do 2.times { repository.ancestor?(commit.id, ancestor.id) } end - it 'increments a counter with cache hits' do - counter = Gitlab::Metrics.counter(:repository_ancestor_calls_total, 'Repository ancestor calls') - - expect do - 2.times { repository.ancestor?(commit.id, ancestor.id) } - end.to change { counter.get(cache_hit: 'true') }.by(1) - .and change { counter.get(cache_hit: 'false') }.by(1) - end - it 'returns the value from the request store' do repository.__send__(:request_store_cache).write(cache_key, "it's apparent") diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index b3189974440..83df54bc2ff 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -33,7 +33,7 @@ describe Ci::RetryBuildService do job_artifacts_sast job_artifacts_dependency_scanning job_artifacts_container_scanning job_artifacts_dast job_artifacts_license_management job_artifacts_license_scanning - job_artifacts_performance + job_artifacts_performance job_artifacts_lsif job_artifacts_codequality job_artifacts_metrics scheduled_at job_variables waiting_for_resource_at job_artifacts_metrics_referee job_artifacts_network_referee].freeze diff --git a/spec/services/merge_requests/reload_diffs_service_spec.rb b/spec/services/merge_requests/reload_diffs_service_spec.rb index c450fc0a7dc..d2444af1b0f 100644 --- a/spec/services/merge_requests/reload_diffs_service_spec.rb +++ b/spec/services/merge_requests/reload_diffs_service_spec.rb @@ -33,34 +33,13 @@ describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_cachin end context 'cache clearing' do - context 'using Gitlab::Diff::DeprecatedHighlightCache' do - before do - stub_feature_flags(hset_redis_diff_caching: false) - end + it 'clears the cache for older diffs on the merge request' do + old_diff = merge_request.merge_request_diff + old_cache_key = old_diff.diffs_collection.cache_key - it 'clears the cache for older diffs on the merge request' do - old_diff = merge_request.merge_request_diff - old_cache_key = old_diff.diffs_collection.cache_key + expect_any_instance_of(Redis).to receive(:del).with(old_cache_key).and_call_original - expect(Rails.cache).to receive(:delete).with(old_cache_key).and_call_original - - subject.execute - end - end - - context 'using Gitlab::Diff::HighlightCache' do - before do - stub_feature_flags(hset_redis_diff_caching: true) - end - - it 'clears the cache for older diffs on the merge request' do - old_diff = merge_request.merge_request_diff - old_cache_key = old_diff.diffs_collection.cache_key - - expect_any_instance_of(Redis).to receive(:del).with(old_cache_key).and_call_original - - subject.execute - end + subject.execute end it 'avoids N+1 queries', :request_store do diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index c5e2fe8de12..7ba069d1e39 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -87,28 +87,10 @@ describe Notes::CreateService do .to receive(:unfolded_diff?) { true } end - context 'using Gitlab::Diff::DeprecatedHighlightCache' do - before do - stub_feature_flags(hset_redis_diff_caching: false) - end - - it 'clears noteable diff cache when it was unfolded for the note position' do - expect_any_instance_of(Gitlab::Diff::DeprecatedHighlightCache).to receive(:clear) - - described_class.new(project_with_repo, user, new_opts).execute - end - end + it 'clears noteable diff cache when it was unfolded for the note position' do + expect_any_instance_of(Gitlab::Diff::HighlightCache).to receive(:clear) - context 'using Gitlab::Diff::HighlightCache' do - before do - stub_feature_flags(hset_redis_diff_caching: true) - end - - it 'clears noteable diff cache when it was unfolded for the note position' do - expect_any_instance_of(Gitlab::Diff::HighlightCache).to receive(:clear) - - described_class.new(project_with_repo, user, new_opts).execute - end + described_class.new(project_with_repo, user, new_opts).execute end it 'does not clear cache when note is not the first of the discussion' do diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb index 0c55e9de045..0e8ee6f66f5 100644 --- a/spec/support/shared_examples/models/mentionable_shared_examples.rb +++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb @@ -229,16 +229,17 @@ RSpec.shared_examples 'mentions in description' do |mentionable_type| context 'when mentionable description contains mentions' do let(:user) { create(:user) } + let(:user2) { create(:user) } let(:group) { create(:group) } - let(:mentionable_desc) { "#{user.to_reference} some description #{group.to_reference(full: true)} and @all" } + let(:mentionable_desc) { "#{user.to_reference} #{user2.to_reference} #{user.to_reference} some description #{group.to_reference(full: true)} and #{user2.to_reference} @all" } let(:mentionable) { create(mentionable_type, description: mentionable_desc) } it 'stores mentions' do add_member(user) expect(mentionable.user_mentions.count).to eq 1 - expect(mentionable.referenced_users).to match_array([user]) + expect(mentionable.referenced_users).to match_array([user, user2]) expect(mentionable.referenced_projects(user)).to match_array([mentionable.project].compact) # epic.project is nil, and we want empty [] expect(mentionable.referenced_groups(user)).to match_array([group]) end @@ -249,8 +250,9 @@ end RSpec.shared_examples 'mentions in notes' do |mentionable_type| context 'when mentionable notes contain mentions' do let(:user) { create(:user) } + let(:user2) { create(:user) } let(:group) { create(:group) } - let(:note_desc) { "#{user.to_reference} and #{group.to_reference(full: true)} and @all" } + let(:note_desc) { "#{user.to_reference} #{user2.to_reference} #{user.to_reference} and #{group.to_reference(full: true)} and #{user2.to_reference} @all" } let!(:mentionable) { note.noteable } before do @@ -261,7 +263,7 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type| it 'returns all mentionable mentions' do expect(mentionable.user_mentions.count).to eq 1 - expect(mentionable.referenced_users).to eq [user] + expect(mentionable.referenced_users).to eq [user, user2] expect(mentionable.referenced_projects(user)).to eq [mentionable.project].compact # epic.project is nil, and we want empty [] expect(mentionable.referenced_groups(user)).to eq [group] end |