-
+
{{ branchLabel }}
diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
index ebb09947663..a803afeb901 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
@@ -111,7 +111,7 @@ export default {
diff --git a/app/assets/javascripts/monitoring/components/links_section.vue b/app/assets/javascripts/monitoring/components/links_section.vue
index 3f9f57d4ac1..fb5ab12916e 100644
--- a/app/assets/javascripts/monitoring/components/links_section.vue
+++ b/app/assets/javascripts/monitoring/components/links_section.vue
@@ -15,7 +15,7 @@ export default {
{{ $options.i18n.plan.description }}
+
+
+
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue
index 3d2a8eed9d4..6cd3bbc359b 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue
@@ -61,7 +61,7 @@ export default {
-
+
{{ title }}
{{ description }}
{{ actionLabel }}
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
index ba456131327..9e204aa6746 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
@@ -63,6 +63,15 @@ export const ACTION_LABELS = {
section: 'deploy',
position: 1,
},
+ issueCreated: {
+ title: s__('LearnGitLab|Create an issue'),
+ actionLabel: s__('LearnGitLab|Create an issue'),
+ description: s__(
+ 'LearnGitLab|Create/import issues (tickets) to collaborate on ideas and plan work.',
+ ),
+ section: 'plan',
+ position: 0,
+ },
};
export const ACTION_SECTIONS = {
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js
index c7d973b5985..216b6c6737c 100644
--- a/app/assets/javascripts/pipelines/components/graph/constants.js
+++ b/app/assets/javascripts/pipelines/components/graph/constants.js
@@ -13,5 +13,6 @@ export const GRAPHQL = 'graphql';
export const STAGE_VIEW = 'stage';
export const LAYER_VIEW = 'layer';
+export const VIEW_TYPE_KEY = 'pipeline_graph_view_type';
export const IID_FAILURE = 'missing_iid';
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
index 71e6c26ff22..0bc6d883245 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -2,11 +2,12 @@
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import { __ } from '~/locale';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
import { reportToSentry } from '../../utils';
import { listByLayers } from '../parsing_utils';
-import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW } from './constants';
+import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants';
import PipelineGraph from './graph_component.vue';
import GraphViewSelector from './graph_view_selector.vue';
import {
@@ -22,6 +23,7 @@ export default {
GlAlert,
GlLoadingIcon,
GraphViewSelector,
+ LocalStorageSync,
PipelineGraph,
},
mixins: [glFeatureFlagMixin()],
@@ -143,7 +145,7 @@ export default {
return this.$apollo.queries.pipeline.loading && !this.pipeline;
},
showGraphViewSelector() {
- return Boolean(this.glFeatures.pipelineGraphLayersView && this.pipeline);
+ return Boolean(this.glFeatures.pipelineGraphLayersView && this.pipeline?.usesNeeds);
},
},
mounted() {
@@ -184,6 +186,7 @@ export default {
this.currentViewType = type;
},
},
+ viewTypeKey: VIEW_TYPE_KEY,
};
@@ -191,11 +194,17 @@ export default {
{{ alert.text }}
-
+
+
+
{{ $options.i18n.labelText }}
-
+
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index 0a73239709a..6ef0ed6d365 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -3,18 +3,13 @@
class Profiles::NotificationsController < Profiles::ApplicationController
feature_category :users
- # rubocop: disable CodeReuse/ActiveRecord
def show
@user = current_user
@user_groups = user_groups
@group_notifications = UserGroupNotificationSettingsFinder.new(current_user, user_groups).execute
-
- @project_notifications = current_user.notification_settings.for_projects.order(:id)
- .preload_source_route
- .select { |notification| current_user.can?(:read_project, notification.source) }
+ @project_notifications = project_notifications_with_preloaded_associations
@global_notification_setting = current_user.global_notification_setting
end
- # rubocop: enable CodeReuse/ActiveRecord
def update
result = Users::UpdateService.new(current_user, user_params.merge(user: current_user)).execute
@@ -37,4 +32,20 @@ class Profiles::NotificationsController < Profiles::ApplicationController
def user_groups
GroupsFinder.new(current_user, all_available: false).execute.order_name_asc.page(params[:page])
end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def project_notifications_with_preloaded_associations
+ project_notifications = current_user
+ .notification_settings
+ .for_projects
+ .order_by_id_asc
+ .preload_source_route
+
+ projects = project_notifications.map(&:source)
+ ActiveRecord::Associations::Preloader.new.preload(projects, { namespace: [:route, :owner], group: [] })
+ Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute
+
+ project_notifications.select { |notification| current_user.can?(:read_project, notification.source) }
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/graphql/queries/pipelines/get_pipeline_details.query.graphql b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql
index 92323923266..959bf7dc91d 100644
--- a/app/graphql/queries/pipelines/get_pipeline_details.query.graphql
+++ b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql
@@ -27,6 +27,7 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
__typename
id
iid
+ usesNeeds
downstream {
__typename
nodes {
diff --git a/app/graphql/resolvers/concerns/board_issue_filterable.rb b/app/graphql/resolvers/concerns/board_issue_filterable.rb
index 1541738f46c..1943f9eb070 100644
--- a/app/graphql/resolvers/concerns/board_issue_filterable.rb
+++ b/app/graphql/resolvers/concerns/board_issue_filterable.rb
@@ -18,6 +18,17 @@ module BoardIssueFilterable
end
def set_filter_values(filters)
+ filter_by_assignee(filters)
+ end
+
+ def filter_by_assignee(filters)
+ if filters[:assignee_username] && filters[:assignee_wildcard_id]
+ raise ::Gitlab::Graphql::Errors::ArgumentError, 'Incompatible arguments: assigneeUsername, assigneeWildcardId.'
+ end
+
+ if filters[:assignee_wildcard_id]
+ filters[:assignee_id] = filters.delete(:assignee_wildcard_id)
+ end
end
end
diff --git a/app/graphql/types/boards/assignee_wildcard_id_enum.rb b/app/graphql/types/boards/assignee_wildcard_id_enum.rb
new file mode 100644
index 00000000000..ba9058a78d9
--- /dev/null
+++ b/app/graphql/types/boards/assignee_wildcard_id_enum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+ module Boards
+ class AssigneeWildcardIdEnum < BaseEnum
+ graphql_name 'AssigneeWildcardId'
+ description 'Assignee ID wildcard values'
+
+ value 'NONE', 'No assignee is assigned.'
+ value 'ANY', 'An assignee is assigned.'
+ end
+ end
+end
diff --git a/app/graphql/types/boards/board_issue_input_type.rb b/app/graphql/types/boards/board_issue_input_type.rb
index 3404f24ea12..04f9341c44e 100644
--- a/app/graphql/types/boards/board_issue_input_type.rb
+++ b/app/graphql/types/boards/board_issue_input_type.rb
@@ -18,6 +18,10 @@ module Types
argument :search, GraphQL::STRING_TYPE,
required: false,
description: 'Search query for issue title or description.'
+
+ argument :assignee_wildcard_id, ::Types::Boards::AssigneeWildcardIdEnum,
+ required: false,
+ description: 'Filter by assignee wildcard. Incompatible with assigneeUsername.'
end
end
end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index 3ae9f93a27a..65feea4f6e0 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -84,3 +84,4 @@ module AppearancesHelper
end
AppearancesHelper.prepend_if_ee('EE::AppearancesHelper')
+AppearancesHelper.prepend_if_jh('JH::AppearancesHelper')
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 1115bee0843..6309d5a2d82 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -194,10 +194,16 @@ module ApplicationHelper
end
end
- def promo_host
+ # This needs to be used outside of Rails
+ def self.promo_host
'about.gitlab.com'
end
+ # Convenient method for Rails helper
+ def promo_host
+ ApplicationHelper.promo_host
+ end
+
def promo_url
'https://' + promo_host
end
@@ -406,3 +412,4 @@ module ApplicationHelper
end
ApplicationHelper.prepend_if_ee('EE::ApplicationHelper')
+ApplicationHelper.prepend_if_jh('JH::ApplicationHelper')
diff --git a/app/helpers/learn_gitlab_helper.rb b/app/helpers/learn_gitlab_helper.rb
index 0cb84fe5227..81896fb9fa4 100644
--- a/app/helpers/learn_gitlab_helper.rb
+++ b/app/helpers/learn_gitlab_helper.rb
@@ -24,6 +24,7 @@ module LearnGitlabHelper
private
ACTION_ISSUE_IDS = {
+ issue_created: 4,
git_write: 6,
pipeline_created: 7,
merge_request_created: 9,
diff --git a/app/models/concerns/bulk_member_access_load.rb b/app/models/concerns/bulk_member_access_load.rb
index f44ad474cd5..e252ca36629 100644
--- a/app/models/concerns/bulk_member_access_load.rb
+++ b/app/models/concerns/bulk_member_access_load.rb
@@ -13,13 +13,7 @@ module BulkMemberAccessLoad
raise 'Block is mandatory' unless block_given?
resource_ids = resource_ids.uniq
- key = max_member_access_for_resource_key(resource_klass, memoization_index)
- access = {}
-
- if Gitlab::SafeRequestStore.active?
- Gitlab::SafeRequestStore[key] ||= {}
- access = Gitlab::SafeRequestStore[key]
- end
+ access = load_access_hash(resource_klass, memoization_index)
# Look up only the IDs we need
resource_ids -= access.keys
@@ -39,10 +33,28 @@ module BulkMemberAccessLoad
access
end
+ def merge_value_to_request_store(resource_klass, resource_id, memoization_index, value)
+ max_member_access_for_resource_ids(resource_klass, [resource_id], memoization_index) do
+ { resource_id => value }
+ end
+ end
+
private
def max_member_access_for_resource_key(klass, memoization_index)
"max_member_access_for_#{klass.name.underscore.pluralize}:#{memoization_index}"
end
+
+ def load_access_hash(resource_klass, memoization_index)
+ key = max_member_access_for_resource_key(resource_klass, memoization_index)
+
+ access = {}
+ if Gitlab::SafeRequestStore.active?
+ Gitlab::SafeRequestStore[key] ||= {}
+ access = Gitlab::SafeRequestStore[key]
+ end
+
+ access
+ end
end
end
diff --git a/app/models/concerns/cascading_namespace_setting_attribute.rb b/app/models/concerns/cascading_namespace_setting_attribute.rb
index cd489e3a7f3..2b4a108a9a0 100644
--- a/app/models/concerns/cascading_namespace_setting_attribute.rb
+++ b/app/models/concerns/cascading_namespace_setting_attribute.rb
@@ -25,7 +25,7 @@ module CascadingNamespaceSettingAttribute
class_methods do
def cascading_settings_feature_enabled?
- ::Feature.enabled?(:cascading_namespace_settings, default_enabled: false)
+ ::Feature.enabled?(:cascading_namespace_settings, default_enabled: true)
end
private
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index b9ad78c14fd..774cda2c3e8 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -77,9 +77,14 @@ module HasRepository
def default_branch_from_preferences
return unless empty_repo?
- group_branch_default_name = group&.default_branch_name if respond_to?(:group)
+ (default_branch_from_group_preferences || Gitlab::CurrentSettings.default_branch_name).presence
+ end
- (group_branch_default_name || Gitlab::CurrentSettings.default_branch_name).presence
+ def default_branch_from_group_preferences
+ return unless respond_to?(:group)
+ return unless group
+
+ group.default_branch_name || group.root_ancestor.default_branch_name
end
def reload_default_branch
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 72813b17501..9c3058b4706 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -30,6 +30,8 @@ class NotificationSetting < ApplicationRecord
scope :preload_source_route, -> { preload(source: [:route]) }
+ scope :order_by_id_asc, -> { order(id: :asc) }
+
# NOTE: Applicable unfound_translations.rb also needs to be updated when below events are changed.
EMAIL_EVENTS = [
:new_release,
diff --git a/app/models/preloaders/user_max_access_level_in_projects_preloader.rb b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
new file mode 100644
index 00000000000..671091480ee
--- /dev/null
+++ b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Preloaders
+ # This class preloads the max access level for the user within the given projects and
+ # stores the values in requests store via the ProjectTeam class.
+ class UserMaxAccessLevelInProjectsPreloader
+ def initialize(projects, user)
+ @projects = projects
+ @user = user
+ end
+
+ def execute
+ access_levels = @user
+ .project_authorizations
+ .where(project_id: @projects)
+ .group(:project_id)
+ .maximum(:access_level)
+
+ @projects.each do |project|
+ access_level = access_levels[project.id] || Gitlab::Access::NO_ACCESS
+ ProjectTeam.new(project).write_member_access_for_user_id(@user.id, access_level)
+ end
+ end
+ end
+end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 5b7eded00cd..1a3f362e6a1 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -174,6 +174,10 @@ class ProjectTeam
end
end
+ def write_member_access_for_user_id(user_id, project_access_level)
+ merge_value_to_request_store(User, user_id, project.id, project_access_level)
+ end
+
def max_member_access(user_id)
max_member_access_for_user_ids([user_id])[user_id]
end
diff --git a/app/services/boards/lists/base_update_service.rb b/app/services/boards/lists/base_update_service.rb
new file mode 100644
index 00000000000..faf58e405fc
--- /dev/null
+++ b/app/services/boards/lists/base_update_service.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Boards
+ module Lists
+ class BaseUpdateService < Boards::BaseService
+ def execute(list)
+ if execute_by_params(list)
+ success(list: list)
+ else
+ error(list.errors.messages, 422)
+ end
+ end
+
+ private
+
+ def execute_by_params(list)
+ update_preferences_result = update_preferences(list) if can_read?(list)
+ update_position_result = update_position(list) if can_admin?(list)
+
+ update_preferences_result || update_position_result
+ end
+
+ def update_preferences(list)
+ return unless preferences?
+
+ list.update_preferences_for(current_user, preferences)
+ end
+
+ def update_position(list)
+ return unless position?
+
+ move_service = Boards::Lists::MoveService.new(parent, current_user, params)
+
+ move_service.execute(list)
+ end
+
+ def preferences
+ { collapsed: Gitlab::Utils.to_boolean(params[:collapsed]) }
+ end
+
+ def preferences?
+ params.has_key?(:collapsed)
+ end
+
+ def position?
+ params.has_key?(:position)
+ end
+
+ def can_read?(list)
+ raise NotImplementedError
+ end
+
+ def can_admin?(list)
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/app/services/boards/lists/update_service.rb b/app/services/boards/lists/update_service.rb
index e2d9c371ca2..2e1a6592cd9 100644
--- a/app/services/boards/lists/update_service.rb
+++ b/app/services/boards/lists/update_service.rb
@@ -2,50 +2,7 @@
module Boards
module Lists
- class UpdateService < Boards::BaseService
- def execute(list)
- if execute_by_params(list)
- success(list: list)
- else
- error(list.errors.messages, 422)
- end
- end
-
- private
-
- def execute_by_params(list)
- update_preferences_result = update_preferences(list) if can_read?(list)
- update_position_result = update_position(list) if can_admin?(list)
-
- update_preferences_result || update_position_result
- end
-
- def update_preferences(list)
- return unless preferences?
-
- list.update_preferences_for(current_user, preferences)
- end
-
- def update_position(list)
- return unless position?
-
- move_service = Boards::Lists::MoveService.new(parent, current_user, params)
-
- move_service.execute(list)
- end
-
- def preferences
- { collapsed: Gitlab::Utils.to_boolean(params[:collapsed]) }
- end
-
- def preferences?
- params.has_key?(:collapsed)
- end
-
- def position?
- params.has_key?(:position)
- end
-
+ class UpdateService < Boards::Lists::BaseUpdateService
def can_read?(list)
Ability.allowed?(current_user, :read_issue_board_list, parent)
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index d7e2e678dac..1ce6e8ae2dc 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -31,9 +31,9 @@ module Projects
# Create status notifying the deployment of pages
@status = create_status
+ @status.update_older_statuses_retried! if Feature.enabled?(:ci_fix_commit_status_retried, project, default_enabled: :yaml)
@status.enqueue!
@status.run!
- @status.update_older_statuses_retried! if Feature.enabled?(:ci_fix_commit_status_retried, project, default_enabled: :yaml)
raise InvalidStateError, 'missing pages artifacts' unless build.artifacts?
raise InvalidStateError, 'build SHA is outdated for this ref' unless latest?
diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml
index c4672a5b25e..ef876779ad6 100644
--- a/app/views/devise/passwords/new.html.haml
+++ b/app/views/devise/passwords/new.html.haml
@@ -5,9 +5,9 @@
= render "devise/shared/error_messages", resource: resource
.form-group
= f.label :email
- = f.email_field :email, class: "form-control gl-form-input", required: true, value: params[:user_email], autofocus: true, title: 'Please provide a valid email address.'
+ = f.email_field :email, class: "form-control gl-form-input", required: true, value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.')
.clearfix
- = f.submit "Reset password", class: "gl-button btn-confirm btn"
+ = f.submit _("Reset password"), class: "gl-button btn-confirm btn"
.clearfix.prepend-top-20
= render 'devise/shared/sign_in_link'
diff --git a/app/views/import/shared/_new_project_form.html.haml b/app/views/import/shared/_new_project_form.html.haml
index bfc4e65e23d..561c14dc68a 100644
--- a/app/views/import/shared/_new_project_form.html.haml
+++ b/app/views/import/shared/_new_project_form.html.haml
@@ -5,7 +5,7 @@
.form-group.col-12.col-sm-6
= label_tag :namespace_id, _('Project URL'), class: 'label-bold'
.form-group
- .input-group.flex-nowrap
+ .input-group.gl-flex-nowrap
- if current_user.can_select_namespace?
.input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url }
.input-group-text
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index f6adb213916..4695cd59f32 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -12,7 +12,7 @@
.form-group.project-path.col-sm-6
= f.label :namespace_id, class: 'label-bold' do
%span= s_("Project URL")
- .input-group.flex-nowrap
+ .input-group.gl-flex-nowrap
- if current_user.can_select_namespace?
.input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url }
.input-group-text
diff --git a/app/views/shared/projects/_search_bar.html.haml b/app/views/shared/projects/_search_bar.html.haml
index 6c3a6ce809f..8ec11d9cfbb 100644
--- a/app/views/shared/projects/_search_bar.html.haml
+++ b/app/views/shared/projects/_search_bar.html.haml
@@ -3,7 +3,7 @@
- flex_grow_and_shrink_xs = 'd-flex flex-xs-grow-1 flex-xs-shrink-1 flex-grow-0 flex-shrink-0'
.filtered-search-block.row-content-block.bt-0
- .filtered-search-wrapper.d-flex.flex-nowrap.flex-column.flex-sm-wrap.flex-sm-row.flex-xl-nowrap
+ .filtered-search-wrapper.d-flex.gl-flex-nowrap.flex-column.flex-sm-wrap.flex-sm-row.flex-xl-nowrap
- unless project_tab_filter == :starred
.filtered-search-nav.mb-2.mb-lg-0{ class: flex_grow_and_shrink_xs }
= render 'dashboard/projects/nav', project_tab_filter: project_tab_filter
diff --git a/changelogs/unreleased/21043-fix-more-n-plus-one-queries.yml b/changelogs/unreleased/21043-fix-more-n-plus-one-queries.yml
new file mode 100644
index 00000000000..15c867e107a
--- /dev/null
+++ b/changelogs/unreleased/21043-fix-more-n-plus-one-queries.yml
@@ -0,0 +1,6 @@
+---
+title: Eliminate N+1 database queries on the user notifications page within the project
+ notifications section
+merge_request: 59029
+author:
+type: performance
diff --git a/changelogs/unreleased/280781-support-assignee-wildcard-filters-board-issues-graphql.yml b/changelogs/unreleased/280781-support-assignee-wildcard-filters-board-issues-graphql.yml
new file mode 100644
index 00000000000..29fcceb5d96
--- /dev/null
+++ b/changelogs/unreleased/280781-support-assignee-wildcard-filters-board-issues-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Support filtering by assignee wildcard in GraphQL board list issues query
+merge_request: 58996
+author:
+type: added
diff --git a/changelogs/unreleased/293953_inherit_default_branch_name_for_subgroups.yml b/changelogs/unreleased/293953_inherit_default_branch_name_for_subgroups.yml
new file mode 100644
index 00000000000..cbb9c3a2744
--- /dev/null
+++ b/changelogs/unreleased/293953_inherit_default_branch_name_for_subgroups.yml
@@ -0,0 +1,5 @@
+---
+title: Inherit default branch name for subgroups
+merge_request: 57101
+author:
+type: fixed
diff --git a/changelogs/unreleased/Externalize-strings-in-passwords-new-html-haml.yml b/changelogs/unreleased/Externalize-strings-in-passwords-new-html-haml.yml
new file mode 100644
index 00000000000..34ff21e55c8
--- /dev/null
+++ b/changelogs/unreleased/Externalize-strings-in-passwords-new-html-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings in passwords/new.html.haml
+merge_request: 58236
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/dblessing_cascading_settings_default_enabled.yml b/changelogs/unreleased/dblessing_cascading_settings_default_enabled.yml
new file mode 100644
index 00000000000..1ec0672a962
--- /dev/null
+++ b/changelogs/unreleased/dblessing_cascading_settings_default_enabled.yml
@@ -0,0 +1,5 @@
+---
+title: Default enable cascading settings feature flag
+merge_request: 59026
+author:
+type: changed
diff --git a/changelogs/unreleased/id-bump-gon-version.yml b/changelogs/unreleased/id-bump-gon-version.yml
new file mode 100644
index 00000000000..c932da6e3b1
--- /dev/null
+++ b/changelogs/unreleased/id-bump-gon-version.yml
@@ -0,0 +1,5 @@
+---
+title: Update gon gem to 6.4.0
+merge_request: 51210
+author:
+type: other
diff --git a/changelogs/unreleased/id-bump-rspec-rails-to-5-1.yml b/changelogs/unreleased/id-bump-rspec-rails-to-5-1.yml
new file mode 100644
index 00000000000..c2c924636a1
--- /dev/null
+++ b/changelogs/unreleased/id-bump-rspec-rails-to-5-1.yml
@@ -0,0 +1,5 @@
+---
+title: Bump rspec-rails to 5.0.1
+merge_request: 59194
+author:
+type: other
diff --git a/changelogs/unreleased/issue-220040-fix-rails-savebang-gitaly-client-module.yml b/changelogs/unreleased/issue-220040-fix-rails-savebang-gitaly-client-module.yml
new file mode 100644
index 00000000000..54bbf67439d
--- /dev/null
+++ b/changelogs/unreleased/issue-220040-fix-rails-savebang-gitaly-client-module.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Rails/SaveBang Rubocop offenses for gitaly client models
+merge_request: 58089
+author: Huzaifa Iftikhar @huzaifaiftikhar
+type: fixed
diff --git a/changelogs/unreleased/mc-backstage-reschedule-artifac-expiry-date-again.yml b/changelogs/unreleased/mc-backstage-reschedule-artifac-expiry-date-again.yml
new file mode 100644
index 00000000000..ffdd268c086
--- /dev/null
+++ b/changelogs/unreleased/mc-backstage-reschedule-artifac-expiry-date-again.yml
@@ -0,0 +1,5 @@
+---
+title: Schedule artifact expiry backfill again.
+merge_request: 59270
+author:
+type: changed
diff --git a/config/application.rb b/config/application.rb
index 03d96fdf85e..b9792cba793 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -57,21 +57,29 @@ module Gitlab
config.generators.templates.push("#{config.root}/generator_templates")
- if Gitlab.ee?
- ee_paths = config.eager_load_paths.each_with_object([]) do |path, memo|
- ee_path = config.root.join('ee', Pathname.new(path).relative_path_from(config.root))
- memo << ee_path.to_s
+ load_paths = lambda do |dir:|
+ ext_paths = config.eager_load_paths.each_with_object([]) do |path, memo|
+ ext_path = config.root.join(dir, Pathname.new(path).relative_path_from(config.root))
+ memo << ext_path.to_s
end
- ee_paths << "#{config.root}/ee/app/replicators"
+ ext_paths << "#{config.root}/#{dir}/app/replicators"
# Eager load should load CE first
- config.eager_load_paths.push(*ee_paths)
- config.helpers_paths.push "#{config.root}/ee/app/helpers"
+ config.eager_load_paths.push(*ext_paths)
+ config.helpers_paths.push "#{config.root}/#{dir}/app/helpers"
- # Other than Ruby modules we load EE first
- config.paths['lib/tasks'].unshift "#{config.root}/ee/lib/tasks"
- config.paths['app/views'].unshift "#{config.root}/ee/app/views"
+ # Other than Ruby modules we load extensions first
+ config.paths['lib/tasks'].unshift "#{config.root}/#{dir}/lib/tasks"
+ config.paths['app/views'].unshift "#{config.root}/#{dir}/app/views"
+ end
+
+ Gitlab.ee do
+ load_paths.call(dir: 'ee')
+ end
+
+ Gitlab.jh do
+ load_paths.call(dir: 'jh')
end
# Rake tasks ignore the eager loading settings, so we need to set the
diff --git a/config/feature_flags/development/cascading_namespace_settings.yml b/config/feature_flags/development/cascading_namespace_settings.yml
index ca4ad32d7c3..d638f457515 100644
--- a/config/feature_flags/development/cascading_namespace_settings.yml
+++ b/config/feature_flags/development/cascading_namespace_settings.yml
@@ -1,8 +1,8 @@
---
name: cascading_namespace_settings
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55678
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321724
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327230
milestone: '13.11'
type: development
group: group::access
-default_enabled: false
+default_enabled: true
diff --git a/config/initializers/0_inject_enterprise_edition_module.rb b/config/initializers/0_inject_enterprise_edition_module.rb
index 7478727f869..f9c82f45040 100644
--- a/config/initializers/0_inject_enterprise_edition_module.rb
+++ b/config/initializers/0_inject_enterprise_edition_module.rb
@@ -31,6 +31,12 @@ module InjectEnterpriseEditionModule
include(ee_module) if Gitlab.ee?
end
+ def prepend_if_jh(constant, with_descendants: false)
+ return unless Gitlab.jh?
+
+ prepend_module(constant.constantize, with_descendants)
+ end
+
private
def prepend_module(mod, with_descendants)
diff --git a/config/initializers/0_license.rb b/config/initializers/0_license.rb
index ce3103be2e4..3db5ec0a91a 100644
--- a/config/initializers/0_license.rb
+++ b/config/initializers/0_license.rb
@@ -1,10 +1,18 @@
# frozen_string_literal: true
-Gitlab.ee do
+load_license = lambda do |dir:, license_name:|
prefix = ENV['GITLAB_LICENSE_MODE'] == 'test' ? 'test_' : ''
- public_key_file = File.read(Rails.root.join(".#{prefix}license_encryption_key.pub"))
+ public_key_file = File.read(Rails.root.join(dir, ".#{prefix}license_encryption_key.pub"))
public_key = OpenSSL::PKey::RSA.new(public_key_file)
Gitlab::License.encryption_key = public_key
rescue
- warn "WARNING: No valid license encryption key provided."
+ warn "WARNING: No valid #{license_name} encryption key provided."
+end
+
+Gitlab.ee do
+ load_license.call(dir: '.', license_name: 'license')
+end
+
+Gitlab.jh do
+ load_license.call(dir: 'jh', license_name: 'JH license')
end
diff --git a/config/initializers_before_autoloader/000_inflections.rb b/config/initializers_before_autoloader/000_inflections.rb
index 93cf9e01b96..de8f79b9a29 100644
--- a/config/initializers_before_autoloader/000_inflections.rb
+++ b/config/initializers_before_autoloader/000_inflections.rb
@@ -35,5 +35,6 @@ ActiveSupport::Inflector.inflections do |inflect|
vulnerability_feedback
)
inflect.acronym 'EE'
+ inflect.acronym 'JH'
inflect.acronym 'CSP'
end
diff --git a/db/post_migrate/20210413132500_reschedule_artifact_expiry_backfill_again.rb b/db/post_migrate/20210413132500_reschedule_artifact_expiry_backfill_again.rb
new file mode 100644
index 00000000000..b4570c8398b
--- /dev/null
+++ b/db/post_migrate/20210413132500_reschedule_artifact_expiry_backfill_again.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+class RescheduleArtifactExpiryBackfillAgain < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'BackfillArtifactExpiryDate'
+ SWITCH_DATE = Date.new(2020, 06, 22).freeze
+
+ disable_ddl_transaction!
+
+ class JobArtifact < ActiveRecord::Base
+ include EachBatch
+
+ self.inheritance_column = :_type_disabled
+ self.table_name = 'ci_job_artifacts'
+
+ scope :without_expiry_date, -> { where(expire_at: nil) }
+ scope :before_switch, -> { where("date(created_at AT TIME ZONE 'UTC') < ?::date", SWITCH_DATE) }
+ end
+
+ def up
+ Gitlab::BackgroundMigration.steal(MIGRATION) do |job|
+ job.delete
+
+ false
+ end
+
+ queue_background_migration_jobs_by_range_at_intervals(
+ JobArtifact.without_expiry_date.before_switch,
+ MIGRATION,
+ 2.minutes,
+ batch_size: 200_000
+ )
+ end
+
+ def down
+ Gitlab::BackgroundMigration.steal(MIGRATION) do |job|
+ job.delete
+
+ false
+ end
+ end
+end
diff --git a/db/schema_migrations/20210413132500 b/db/schema_migrations/20210413132500
new file mode 100644
index 00000000000..662c7e33ef0
--- /dev/null
+++ b/db/schema_migrations/20210413132500
@@ -0,0 +1 @@
+407806cc168ef9859c9a4f1bd4db7a56aee01367e784ea0767889863b9ace35d
\ No newline at end of file
diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md
index fbecd2571ca..d1ea2978202 100644
--- a/doc/administration/geo/disaster_recovery/index.md
+++ b/doc/administration/geo/disaster_recovery/index.md
@@ -446,6 +446,134 @@ Now we need to make each **secondary** node listen to changes on the new **prima
to [initiate the replication process](../setup/database.md#step-3-initiate-the-replication-process) again but this time
for another **primary** node. All the old replication settings will be overwritten.
+## Promoting a secondary Geo cluster in GitLab Cloud Native Helm Charts
+
+When updating a Cloud Native Geo deployment, the process for updating any node that is external to the secondary Kubernetes cluster does not differ from the non Cloud Native approach. As such, you can always defer to [Promoting a secondary Geo node in single-secondary configurations](#promoting-a-secondary-geo-node-in-single-secondary-configurations) for more information.
+
+The following sections assume you are using the `gitlab` namespace. If you used a different namespace when setting up your cluster, you should also replace `--namespace gitlab` with your namespace.
+
+WARNING:
+In GitLab 13.2 and 13.3, promoting a secondary site to a primary while the
+secondary is paused fails. Do not pause replication before promoting a
+secondary. If the site is paused, be sure to resume before promoting. This
+issue has been fixed in GitLab 13.4 and later.
+
+### Step 1. Permanently disable the **primary** cluster
+
+WARNING:
+If the **primary** site goes offline, there may be data saved on the **primary** site
+that has not been replicated to the **secondary** site. This data should be treated
+as lost if you proceed.
+
+If an outage on the **primary** site happens, you should do everything possible to
+avoid a split-brain situation where writes can occur in two different GitLab
+instances, complicating recovery efforts. So to prepare for the failover, you
+must disable the **primary** site:
+
+- If you have access to the **primary** Kubernetes cluster, connect to it and disable the GitLab webservice and Sidekiq pods:
+
+ ```shell
+ kubectl --namespace gitlab scale deploy gitlab-geo-webservice-default --replicas=0
+ kubectl --namespace gitlab scale deploy gitlab-geo-sidekiq-all-in-1-v1 --replicas=0
+ ```
+
+- If you do not have access to the **primary** Kubernetes cluster, take the cluster offline and
+ prevent it from coming back online by any means at your disposal.
+ Since there are many ways you may prefer to accomplish this, we will avoid a
+ single recommendation. You may need to:
+
+ - Reconfigure the load balancers.
+ - Change DNS records (for example, point the primary DNS record to the
+ **secondary** site to stop usage of the **primary** site).
+ - Stop the virtual servers.
+ - Block traffic through a firewall.
+ - Revoke object storage permissions from the **primary** site.
+ - Physically disconnect a machine.
+
+### Step 2. Promote all **secondary** nodes external to the cluster
+
+WARNING:
+If the secondary site [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
+a point-in-time recovery to the last known state.
+Data that was created on the primary while the secondary was paused will be lost.
+
+1. SSH in to the database node in the **secondary** and trigger PostgreSQL to
+ promote to read-write:
+
+ ```shell
+ sudo gitlab-ctl promote-db
+ ```
+
+ In GitLab 12.8 and earlier, see [Message: `sudo: gitlab-pg-ctl: command not found`](../replication/troubleshooting.md#message-sudo-gitlab-pg-ctl-command-not-found).
+
+1. Edit `/etc/gitlab/gitlab.rb` on the database node in the **secondary** site to
+ reflect its new status as **primary** by removing any lines that enabled the
+ `geo_secondary_role`:
+
+ NOTE:
+ Depending on your architecture these steps will need to be run on any GitLab node that is external to the **secondary** Kubernetes cluster.
+
+ ```ruby
+ ## In pre-11.5 documentation, the role was enabled as follows. Remove this line.
+ geo_secondary_role['enable'] = true
+
+ ## In 11.5+ documentation, the role was enabled as follows. Remove this line.
+ roles ['geo_secondary_role']
+ ```
+
+ After making these changes, [reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) on the database node.
+
+### Step 3. Promote the **secondary** cluster
+
+1. Find the task runner pod:
+
+ ```shell
+ kubectl --namespace gitlab get pods -lapp=task-runner
+ ```
+
+1. Promote the secondary:
+
+ ```shell
+ kubectl --namespace gitlab exec -ti gitlab-geo-task-runner-XXX -- gitlab-rake geo:set_secondary_as_primary
+ ```
+
+1. Update the existing cluster configuration.
+
+ You can retrieve the existing config with Helm:
+
+ ```shell
+ helm --namespace gitlab get values gitlab-geo > gitlab.yaml
+ ```
+
+ The existing config will contain a section for Geo that should resemble:
+
+ ```yaml
+ geo:
+ enabled: true
+ role: secondary
+ nodeName: secondary.example.com
+ psql:
+ host: geo-2.db.example.com
+ port: 5431
+ password:
+ secret: geo
+ key: geo-postgresql-password
+ ```
+
+ To promote the **secondary** cluster to a **primary** cluster, update `role: secondary` to `role: primary`.
+
+ You can remove the entire `psql` section if the cluster will remain as a primary site, this refers to the tracking database and will be ignored whilst the cluster is acting as a primary site.
+
+ Update the cluster with the new config:
+
+ ```shell
+ helm upgrade --install --version gitlab-geo gitlab/gitlab --namespace gitlab -f gitlab.yaml
+ ```
+
+1. Verify you can connect to the newly promoted primary using the URL used previously for the secondary.
+
+1. Success! The secondary has now been promoted to primary.
+
## Troubleshooting
This section was moved to [another location](../replication/troubleshooting.md#fixing-errors-during-a-failover-or-when-promoting-a-secondary-to-a-primary-node).
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index f56349fc9e6..4ba1a85babc 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -139,6 +139,18 @@ The following metrics can be controlled by feature flags:
| `gitlab_method_call_duration_seconds` | `prometheus_metrics_method_instrumentation` |
| `gitlab_view_rendering_duration_seconds` | `prometheus_metrics_view_instrumentation` |
+## Praefect metrics
+
+You can [configure Praefect to report metrics](../../gitaly/praefect.md#praefect).
+These are some of the Praefect metrics served from the `/metrics` path on the [configured port](index.md#changing-the-port-and-address-prometheus-listens-on)
+(9652 by default).
+
+| Metric | Type | Since | Description | Labels |
+| :----- | :--- | ----: | :---------- | :----- |
+| `gitaly_praefect_replication_latency_bucket` | Histogram | 12.10 | The amount of time it takes for replication to complete once the replication job starts. | |
+| `gitaly_praefect_replication_delay_bucket` | Histogram | 12.10 | A measure of how much time passes between when the replication job is created and when it starts. | |
+| `gitaly_praefect_node_latency_bucket` | Histogram | 12.10 | The latency in Gitaly returning health check information to Praefect. This indicates Praefect connection saturation. | |
+
## Sidekiq metrics
Sidekiq jobs may also gather metrics, and these metrics can be accessed if the
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 9fd509d61a8..8abeacd0d0e 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -7525,6 +7525,15 @@ The kind of an approval rule.
| `REGULAR` | A `regular` approval rule. |
| `REPORT_APPROVER` | A `report_approver` approval rule. |
+### `AssigneeWildcardId`
+
+Assignee ID wildcard values.
+
+| Value | Description |
+| ----- | ----------- |
+| `ANY` | An assignee is assigned. |
+| `NONE` | No assignee is assigned. |
+
### `AvailabilityEnum`
User availability status.
diff --git a/doc/development/documentation/site_architecture/release_process.md b/doc/development/documentation/site_architecture/release_process.md
index 4776a41aadb..9329b93bb26 100644
--- a/doc/development/documentation/site_architecture/release_process.md
+++ b/doc/development/documentation/site_architecture/release_process.md
@@ -1,181 +1,8 @@
---
-stage: none
-group: unassigned
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+redirect_to: 'https://about.gitlab.com/handbook/engineering/ux/technical-writing/workflow/#monthly-documentation-releases'
---
-# Monthly release process
+This file was moved to [another location](https://about.gitlab.com/handbook/engineering/ux/technical-writing/workflow/#monthly-documentation-releases).
-When a new GitLab version is released on the 22nd, we release version-specific published
-documentation for the new version.
-
-We complete the process as soon as possible after the GitLab version is announced. The result is:
-
-- The [online published documentation](https://docs.gitlab.com) includes:
- - The three most recent minor releases of the current major version. For example 13.9, 13.8, and
- 13.7.
- - The most recent minor releases of the last two major versions. For example 12.10, and 11.11.
-- Documentation updates after the 22nd are for the next release. The versions drop down
- should have the current milestone with `-pre` appended to it, for example `13.10-pre`.
-
-Each documentation release:
-
-- Has a dedicated branch, named in the format `XX.yy`.
-- Has a Docker image that contains a build of that branch.
-
-For example:
-
-- For [GitLab 13.9](https://docs.gitlab.com/13.9/index.html), the
- [stable branch](https://gitlab.com/gitlab-org/gitlab-docs/-/tree/13.9) and Docker image:
- [`registry.gitlab.com/gitlab-org/gitlab-docs:13.9`](https://gitlab.com/gitlab-org/gitlab-docs/container_registry/631635).
-- For [GitLab 13.8](https://docs.gitlab.com/13.8/index.html), the
- [stable branch](https://gitlab.com/gitlab-org/gitlab-docs/-/tree/13.8) and Docker image:
- [`registry.gitlab.com/gitlab-org/gitlab-docs:13.8`](https://gitlab.com/gitlab-org/gitlab-docs/container_registry/631635).
-
-## Recommended timeline
-
-To minimize problems during the documentation release process, use the following timeline:
-
-- Any time before the 17th of the month:
-
- [Add the charts version](#add-chart-version), so that the documentation is built using the
- [version of the charts project that maps to](https://docs.gitlab.com/charts/installation/version_mappings.html)
- the GitLab release. This step may have been completed already.
-
-- Between the 17th and the 20th of the month:
-
- 1. [Create a stable branch and Docker image](#create-stable-branch-and-docker-image-for-release) for
- the new version.
- 1. [Create a release merge request](#create-release-merge-request) for the new version, which
- updates the version dropdown menu for the current documentation and adds the release to the
- Docker configuration. For example, the
- [release merge request for 13.9](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/1555).
- 1. [Update the three online versions](#update-dropdown-for-online-versions), so that they display the new release on their
- version dropdown menus.
-
-- On the 22nd of the month:
-
- [Merge the release merge requests and run the necessary Docker image builds](#merge-merge-requests-and-run-docker-image-builds).
-
-## Add chart version
-
-To add a new charts version for the release:
-
-1. Make sure you're in the root path of the `gitlab-docs` repository.
-1. Open `content/_data/chart_versions.yaml` and add the new stable branch version using the
- [version mapping](https://docs.gitlab.com/charts/installation/version_mappings.html). Only the
- `major.minor` version is needed.
-1. Create a new merge request and merge it.
-
-NOTE:
-If you have time, add anticipated future mappings to `content/_data/chart_versions.yaml`. This saves
-a step for the next GitLab release.
-
-## Create stable branch and Docker image for release
-
-To create a stable branch and Docker image for the release:
-
-1. Make sure you're in the root path of the `gitlab-docs` repository.
-1. Run the Rake task to create the single version. For example, to create the 13.9 release branch
- and perform others tasks:
-
- ```shell
- ./bin/rake "release:single[13.9]"
- ```
-
- A branch for the release is created, a new `Dockerfile.13.9` is created, and `.gitlab-ci.yml`
- has branches variables updated into a new branch. These files are automatically committed.
-
-1. Push the newly created branch, but **don't create a merge request**. After you push, the
- `image:docs-single` job creates a new Docker image tagged with the name of the branch you created
- earlier. You can see the Docker image in the `registry` environment at
- .
-
-For example, see [the 13.9 release pipeline](https://gitlab.com/gitlab-org/gitlab-docs/-/pipelines/260288747).
-
-Optionally, you can test locally by:
-
-1. Building the image and running it. For example, for GitLab 13.9 documentation:
-
- ```shell
- docker build -t docs:13.9 -f Dockerfile.13.9 .
- docker run -it --rm -p 4000:4000 docs:13.9
- ```
-
-1. Visiting to see if everything works correctly.
-
-## Create release merge request
-
-NOTE:
-An [epic is open](https://gitlab.com/groups/gitlab-org/-/epics/4361) to automate this step.
-
-To create the release merge request for the release:
-
-1. Make sure you're in the root path of the `gitlab-docs` repository.
-1. Create a branch `release-X-Y`. For example:
-
- ```shell
- git checkout master
- git checkout -b release-13-9
- ```
-
-1. Edit `content/_data/versions.yaml` and update the lists of versions to reflect the new release:
-
- - Add the latest version to the `online:` section.
- - Move the oldest version in `online:` to the `offline:` section. There should now be three
- versions in `online:`.
-
-1. Update these Dockerfiles:
-
- - `dockerfiles/Dockerfile.archives`: Add the latest version to the top of the list.
- - `Dockerfile.master`: Remove the oldest version, and add the newest version to the
- top of the list.
-
-1. Commit and push to create the merge request. For example:
-
- ```shell
- git add content/ Dockerfile.master dockerfiles/Dockerfile.archives
- git commit -m "Release 13.9"
- git push origin release-13-9
- ```
-
-Do not merge the release merge request yet.
-
-## Update dropdown for online versions
-
-To update `content/_data/versions.yaml` for all online versions (stable branches `X.Y` of the
-`gitlab-docs` project). For example:
-
-- The merge request to [update the 13.9 version dropdown menu for the 13.9 release](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/1556).
-- The merge request to [update the 13.8 version dropdown menu for the 13.9 release](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/1557).
-- The merge request to [update the 13.7 version dropdown menu for the 13.9 release](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/1558).
-
-1. Run the Rake task that creates all of the necessary merge requests to update the dropdowns. For
- example, for the 13.9 release:
-
- ```shell
- git checkout release-13-9
- ./bin/rake release:dropdowns
- ```
-
-1. [Visit the merge requests page](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests?label_name%5B%5D=release)
- to check that their pipelines pass.
-
-Do not merge these merge requests yet.
-
-## Merge merge requests and run Docker image builds
-
-The merge requests for the dropdowns should now all be merged into their respective stable branches.
-Each merge triggers a new pipeline for each stable branch. Wait for the stable branch pipelines to
-complete, then:
-
-1. Check the [pipelines page](https://gitlab.com/gitlab-org/gitlab-docs/pipelines)
- and make sure all stable branches have green pipelines.
-1. After all the pipelines succeed:
- 1. Merge all of the [dropdown merge requests](#update-dropdown-for-online-versions).
- 1. Merge the [release merge request](#create-release-merge-request).
-1. Finally, run the
- [`Build docker images weekly` pipeline](https://gitlab.com/gitlab-org/gitlab-docs/pipeline_schedules)
- that builds the `:latest` and `:archives` Docker images.
-
-As the last step in the scheduled pipeline, the documentation site deploys with all new versions.
+
+
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 81014b7624c..35e7824721a 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -23,6 +23,8 @@ when no license is active. So EE features always should be guarded by
`project.feature_available?` or `group.feature_available?` (or
`License.feature_available?` if it is a system-wide feature).
+Frontend features should be guarded by pushing a flag from the backend by [using `push_licensed_feature`](licensed_feature_availability.md#restricting-frontend-features), and checked using `this.glFeatures.someFeature` in the frontend.
+
CE specs should remain untouched as much as possible and extra specs
should be added for EE. Licensed features can be stubbed using the
spec helper `stub_licensed_features` in `EE::LicenseHelpers`.
diff --git a/doc/development/feature_flags/index.md b/doc/development/feature_flags/index.md
index f6e398f1074..560e4f8cb90 100644
--- a/doc/development/feature_flags/index.md
+++ b/doc/development/feature_flags/index.md
@@ -292,8 +292,7 @@ end
### Frontend
-Use the `push_frontend_feature_flag` method for frontend code, which is
-available to all controllers that inherit from `ApplicationController`. You can use
+Use the `push_frontend_feature_flag` method which is available to all controllers that inherit from `ApplicationController`. You can use
this method to expose the state of a feature flag, for example:
```ruby
diff --git a/doc/development/licensed_feature_availability.md b/doc/development/licensed_feature_availability.md
index a9fc0414297..10e6d717a18 100644
--- a/doc/development/licensed_feature_availability.md
+++ b/doc/development/licensed_feature_availability.md
@@ -41,3 +41,16 @@ the instance license.
```ruby
License.feature_available?(:feature_symbol)
```
+
+## Restricting frontend features
+
+To restrict frontend features based on the license, use `push_licensed_feature`.
+The frontend can then access this via `this.glFeatures`:
+
+```ruby
+before_action do
+ push_licensed_feature(:feature_symbol)
+ # or by project/namespace
+ push_licensed_feature(:feature_symbol, project)
+end
+```
diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md
index faf6f9e6f48..490397cdf1b 100644
--- a/doc/integration/oauth_provider.md
+++ b/doc/integration/oauth_provider.md
@@ -58,6 +58,8 @@ To add a new application for your user:
## Group owned applications
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16227) in GitLab 13.11.
+
To add a new application for a group:
1. Navigate to the desired group.
diff --git a/doc/user/admin_area/geo_nodes.md b/doc/user/admin_area/geo_nodes.md
index f41170da975..e5132ef4e96 100644
--- a/doc/user/admin_area/geo_nodes.md
+++ b/doc/user/admin_area/geo_nodes.md
@@ -70,6 +70,12 @@ breaking communication between **primary** and **secondary** nodes when using
HTTPS, customize your Internal URL to point to a load balancer with TLS
terminated at the load balancer.
+WARNING:
+Starting with GitLab 13.3 and [until 13.11](https://gitlab.com/gitlab-org/gitlab/-/issues/325522),
+using an internal URL that is not accessible to the users will result in the
+OAuth authorization flow not working properly, as the users will get redirected
+to the internal URL instead of the external one.
+
## Multiple secondary nodes behind a load balancer
In GitLab 11.11, **secondary** nodes can use identical external URLs as long as
diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md
index c45ce7a02d9..3acef242cef 100644
--- a/doc/user/project/description_templates.md
+++ b/doc/user/project/description_templates.md
@@ -51,8 +51,10 @@ directory in your repository. Commit and push to your default branch.
To create a Markdown file:
-1. Click the `+` button next to `master` and click **New file**.
-1. Add the name of your issue template to the **File name** text field next to `master`.
+1. In a project, go to **Repository**.
+1. Next to the default branch, select the **{plus}** button.
+1. Select **New file**.
+1. Next to the default branch, in the **File name** field, add the name of your issue template.
Make sure that your file has the `.md` extension, for
example `feature_request.md` or `Feature Request.md`.
1. Commit and push to your default branch.
@@ -61,9 +63,12 @@ If you don't have a `.gitlab/issue_templates` directory in your repository, you
To create the `.gitlab/issue_templates` directory:
-1. Click the `+` button next to `master` and select **New directory**.
+1. In a project, go to **Repository**.
+1. Next to the default branch, select the **{plus}** button.
+1. Select **New directory**.
1. Name this new directory `.gitlab` and commit to your default branch.
-1. Click the `+` button next to `master` again and select **New directory**.
+1. Next to the default branch, select the **{plus}** button.
+1. Select **New directory**.
1. Name your directory `issue_templates` and commit to your default branch.
To check if this has worked correctly, [create a new issue](issues/managing_issues.md#create-a-new-issue)
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index 62f61c01e19..bcfd7f5e5d9 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -65,6 +65,71 @@ can now create their own.
New compliance framework labels can be created and updated using GraphQL.
+#### Compliance pipeline configuration **(ULTIMATE)**
+
+> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3156) in GitLab 13.9.
+> - [Deployed behind a feature flag](../../feature_flags.md).
+> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/300324) in GitLab 13.11.
+> - Enabled on GitLab.com.
+> - Recommended for production use.
+
+WARNING:
+This feature might not be available to you. Check the **version history** note above for details.
+
+Group owners can use the compliance pipeline configuration to define compliance requirements
+such as scans or tests, and enforce them in individual projects.
+
+The [custom compliance framework](#custom-compliance-frameworks) feature allows group owners to specify the location
+of a compliance pipeline configuration stored and managed in a dedicated project, distinct from a developer's project.
+
+When you set up the compliance pipeline configuration field, use the
+`file@group/project` format. For example, you can configure
+`.compliance-gitlab-ci.yml@compliance-group/compliance-project`.
+This field is inherited by projects where the compliance framework label is applied. The result
+forces the project to run the compliance configurations.
+
+When a project with a custom label executes a pipeline, it begins by evaluating the compliance pipeline configuration.
+The custom pipeline configuration can then execute any included individual project configuration.
+
+The user running the pipeline in the project should at least have Reporter access to the compliance project.
+
+Example `.compliance-gitlab-ci.yml`
+
+```yaml
+stages: # Allows compliance team to control the ordering and interweaving of stages/jobs
+- pre-compliance
+- build
+- test
+- pre-deploy-compliance
+- deploy
+- post-compliance
+
+variables: # can be overriden by a developer's local .gitlab-ci.yml
+ FOO: sast
+
+sast: # none of these attributes can be overriden by a developer's local .gitlab-ci.yml
+ variables:
+ FOO: sast
+ stage: pre-compliance
+ script:
+ - echo "running $FOO"
+
+sanity check:
+ stage: pre-deploy-compliance
+ script:
+ - echo "running $FOO"
+
+
+audit trail:
+ stage: post-compliance
+ script:
+ - echo "running $FOO"
+
+include: # Execute individual project's configuration
+ project: '$CI_PROJECT_PATH'
+ file: '$CI_PROJECT_CONFIG_PATH'
+```
+
### Sharing and permissions
For your repository, you can set up features such as public access, repository features,
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index 11ecfb951aa..ddf08c8dc20 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -108,10 +108,21 @@ module Gitlab
!%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
end
+ def self.jh?
+ @is_jh ||=
+ ee? &&
+ root.join('jh').exist? &&
+ !%w[true 1].include?(ENV['EE_ONLY'].to_s)
+ end
+
def self.ee
yield if ee?
end
+ def self.jh
+ yield if jh?
+ end
+
def self.http_proxy_env?
HTTP_PROXY_ENV_VARS.any? { |name| ENV[name] }
end
diff --git a/lib/gitlab/subscription_portal.rb b/lib/gitlab/subscription_portal.rb
index a918e7bec80..3072210d7c8 100644
--- a/lib/gitlab/subscription_portal.rb
+++ b/lib/gitlab/subscription_portal.rb
@@ -6,6 +6,11 @@ module Gitlab
::Gitlab.dev_or_test_env? ? 'https://customers.stg.gitlab.com' : 'https://customers.gitlab.com'
end
- SUBSCRIPTIONS_URL = ENV.fetch('CUSTOMER_PORTAL_URL', default_subscriptions_url).freeze
+ def self.subscriptions_url
+ ENV.fetch('CUSTOMER_PORTAL_URL', default_subscriptions_url)
+ end
end
end
+
+Gitlab::SubscriptionPortal.prepend_if_jh('JH::Gitlab::SubscriptionPortal')
+Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL = Gitlab::SubscriptionPortal.subscriptions_url.freeze
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c7df470f207..8371a6f2812 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8804,9 +8804,24 @@ msgstr ""
msgid "Corpus Management|Are you sure you want to delete the corpus?"
msgstr ""
+msgid "CorpusManagement|Actions"
+msgstr ""
+
+msgid "CorpusManagement|Corpus are used in fuzz testing as mutation source to Improve future testing."
+msgstr ""
+
+msgid "CorpusManagement|Corpus name"
+msgstr ""
+
msgid "CorpusManagement|Fuzz testing corpus management"
msgstr ""
+msgid "CorpusManagement|Last updated"
+msgstr ""
+
+msgid "CorpusManagement|Last used"
+msgstr ""
+
msgid "CorpusManagement|Latest Job:"
msgstr ""
@@ -8816,6 +8831,9 @@ msgstr ""
msgid "CorpusManagement|Not Set"
msgstr ""
+msgid "CorpusManagement|Target"
+msgstr ""
+
msgid "CorpusManagement|Total Size: %{totalSize}"
msgstr ""
@@ -18397,6 +18415,9 @@ msgstr ""
msgid "Learn GitLab|Trial only"
msgstr ""
+msgid "Learn More"
+msgstr ""
+
msgid "Learn how to %{link_start}contribute to the built-in templates%{link_end}"
msgstr ""
@@ -18475,12 +18496,18 @@ msgstr ""
msgid "LearnGitLab|Create a workflow for your new workspace, and learn how GitLab features work together:"
msgstr ""
+msgid "LearnGitLab|Create an issue"
+msgstr ""
+
msgid "LearnGitLab|Create or import a repository"
msgstr ""
msgid "LearnGitLab|Create or import your first repository into your new project."
msgstr ""
+msgid "LearnGitLab|Create/import issues (tickets) to collaborate on ideas and plan work."
+msgstr ""
+
msgid "LearnGitLab|Deploy"
msgstr ""
@@ -26722,6 +26749,9 @@ msgstr ""
msgid "Reset key"
msgstr ""
+msgid "Reset password"
+msgstr ""
+
msgid "Reset registration token"
msgstr ""
@@ -29023,9 +29053,6 @@ msgstr ""
msgid "Something went wrong on our end"
msgstr ""
-msgid "Something went wrong on our end while loading the code quality diff."
-msgstr ""
-
msgid "Something went wrong on our end."
msgstr ""
@@ -30997,7 +31024,7 @@ msgstr ""
msgid "The merge request can now be merged."
msgstr ""
-msgid "The merge request has made changes to this file that affect the number of code quality violations in it."
+msgid "The merge request has been updated, and the number of code quality violations in this file has changed."
msgstr ""
msgid "The metric must be one of %{metrics}."
diff --git a/package.json b/package.json
index c5b6d7da605..7524f79693f 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.188.0",
"@gitlab/tributejs": "1.0.0",
- "@gitlab/ui": "29.3.0",
+ "@gitlab/ui": "29.4.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-4",
"@rails/ujs": "^6.0.3-4",
@@ -155,7 +155,7 @@
"vuex": "^3.6.0",
"web-vitals": "^0.2.4",
"webpack": "^4.46.0",
- "webpack-bundle-analyzer": "^4.4.0",
+ "webpack-bundle-analyzer": "^4.4.1",
"webpack-cli": "^3.3.12",
"webpack-stats-plugin": "^0.3.1",
"worker-loader": "^2.0.0",
diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh
index 6ec19327942..e23231e429b 100644
--- a/scripts/rspec_helpers.sh
+++ b/scripts/rspec_helpers.sh
@@ -97,6 +97,10 @@ function rspec_paralellized_job() {
spec_folder_prefix="ee/"
fi
+ if [[ "${test_tool}" =~ "-jh" ]]; then
+ spec_folder_prefix="jh/"
+ fi
+
export KNAPSACK_LOG_LEVEL="debug"
export KNAPSACK_REPORT_PATH="knapsack/${report_name}_report.json"
diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb
index 0ce2854571d..1ebf4363ba6 100644
--- a/spec/controllers/profiles/notifications_controller_spec.rb
+++ b/spec/controllers/profiles/notifications_controller_spec.rb
@@ -21,6 +21,30 @@ RSpec.describe Profiles::NotificationsController do
expect(response).to render_template :show
end
+ context 'when personal projects are present', :request_store do
+ let!(:personal_project_1) { create(:project, namespace: user.namespace) }
+
+ context 'N+1 query check' do
+ render_views
+
+ it 'does not have an N+1' do
+ sign_in(user)
+
+ get :show
+
+ control = ActiveRecord::QueryRecorder.new do
+ get :show
+ end
+
+ create_list(:project, 2, namespace: user.namespace)
+
+ expect do
+ get :show
+ end.not_to exceed_query_limit(control)
+ end
+ end
+ end
+
context 'with groups that do not have notification preferences' do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
diff --git a/spec/deprecation_toolkit_env.rb b/spec/deprecation_toolkit_env.rb
index f4ead6d5f01..e49b3074b06 100644
--- a/spec/deprecation_toolkit_env.rb
+++ b/spec/deprecation_toolkit_env.rb
@@ -56,7 +56,6 @@ module DeprecationToolkitEnv
def self.allowed_kwarg_warning_paths
%w[
activerecord-6.0.3.4/lib/active_record/migration.rb
- devise-4.7.3/lib/devise/test/controller_helpers.rb
activesupport-6.0.3.4/lib/active_support/cache.rb
batch-loader-1.4.0/lib/batch_loader/graphql.rb
carrierwave-1.3.1/lib/carrierwave/sanitized_file.rb
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index aa848c25349..6732ef575d4 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -596,7 +596,7 @@ RSpec.describe 'Admin updates settings' do
context 'Nav bar' do
it 'shows default help links in nav' do
- default_support_url = 'https://about.gitlab.com/getting-help/'
+ default_support_url = "https://#{ApplicationHelper.promo_host}/getting-help/"
visit root_dashboard_path
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
index 4feddb939c6..a01ec1db35c 100644
--- a/spec/frontend/diffs/components/compare_versions_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -211,21 +211,22 @@ describe('CompareVersions', () => {
});
describe('prev commit', () => {
- const { location } = window;
-
beforeAll(() => {
- delete window.location;
- window.location = { href: `${TEST_HOST}?commit_id=${mrCommit.id}` };
+ global.jsdom.reconfigure({
+ url: `${TEST_HOST}?commit_id=${mrCommit.id}`,
+ });
+ });
+
+ afterAll(() => {
+ global.jsdom.reconfigure({
+ url: TEST_HOST,
+ });
});
beforeEach(() => {
jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
});
- afterAll(() => {
- window.location = location;
- });
-
it('uses the correct href', () => {
const link = getPrevCommitNavElement();
@@ -253,21 +254,22 @@ describe('CompareVersions', () => {
});
describe('next commit', () => {
- const { location } = window;
-
beforeAll(() => {
- delete window.location;
- window.location = { href: `${TEST_HOST}?commit_id=${mrCommit.id}` };
+ global.jsdom.reconfigure({
+ url: `${TEST_HOST}?commit_id=${mrCommit.id}`,
+ });
+ });
+
+ afterAll(() => {
+ global.jsdom.reconfigure({
+ url: TEST_HOST,
+ });
});
beforeEach(() => {
jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
});
- afterAll(() => {
- window.location = location;
- });
-
it('uses the correct href', () => {
const link = getNextCommitNavElement();
diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js
index 5682b29d697..0bc1bd40f06 100644
--- a/spec/frontend/diffs/components/diff_row_spec.js
+++ b/spec/frontend/diffs/components/diff_row_spec.js
@@ -4,6 +4,7 @@ import Vuex from 'vuex';
import DiffRow from '~/diffs/components/diff_row.vue';
import { mapParallel } from '~/diffs/components/diff_row_utils';
import diffsModule from '~/diffs/store/modules';
+import { findInteropAttributes } from '../find_interop_attributes';
import diffFileMockData from '../mock_data/diff_file';
describe('DiffRow', () => {
@@ -211,4 +212,20 @@ describe('DiffRow', () => {
expect(coverage.classes('no-coverage')).toBeFalsy();
});
});
+
+ describe('interoperability', () => {
+ it.each`
+ desc | line | inline | leftSide | rightSide
+ ${'with inline and new_line'} | ${{ left: { old_line: 3, new_line: 5, type: 'new' } }} | ${true} | ${{ type: 'new', line: '5', oldLine: '3', newLine: '5' }} | ${null}
+ ${'with inline and no new_line'} | ${{ left: { old_line: 3, type: 'old' } }} | ${true} | ${{ type: 'old', line: '3', oldLine: '3' }} | ${null}
+ ${'with parallel and no right side'} | ${{ left: { old_line: 3, new_line: 5 } }} | ${false} | ${{ type: 'old', line: '3', oldLine: '3' }} | ${null}
+ ${'with parallel and no left side'} | ${{ right: { old_line: 3, new_line: 5 } }} | ${false} | ${null} | ${{ type: 'new', line: '5', newLine: '5' }}
+ ${'with parallel and right side'} | ${{ left: { old_line: 3 }, right: { new_line: 5 } }} | ${false} | ${{ type: 'old', line: '3', oldLine: '3' }} | ${{ type: 'new', line: '5', newLine: '5' }}
+ `('$desc, sets interop data attributes', ({ line, inline, leftSide, rightSide }) => {
+ const wrapper = createWrapper({ props: { line, inline } });
+
+ expect(findInteropAttributes(wrapper, '[data-testid="left-side"]')).toEqual(leftSide);
+ expect(findInteropAttributes(wrapper, '[data-testid="right-side"]')).toEqual(rightSide);
+ });
+ });
});
diff --git a/spec/frontend/diffs/components/inline_diff_table_row_spec.js b/spec/frontend/diffs/components/inline_diff_table_row_spec.js
index 28b3055b58c..66b63a7a1d0 100644
--- a/spec/frontend/diffs/components/inline_diff_table_row_spec.js
+++ b/spec/frontend/diffs/components/inline_diff_table_row_spec.js
@@ -3,6 +3,7 @@ import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import { mapInline } from '~/diffs/components/diff_row_utils';
import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue';
import { createStore } from '~/mr_notes/stores';
+import { findInteropAttributes } from '../find_interop_attributes';
import discussionsMockData from '../mock_data/diff_discussions';
import diffFileMockData from '../mock_data/diff_file';
@@ -310,4 +311,16 @@ describe('InlineDiffTableRow', () => {
});
});
});
+
+ describe('interoperability', () => {
+ it.each`
+ desc | line | expectation
+ ${'with type old'} | ${{ ...thisLine, type: 'old', old_line: 3, new_line: 5 }} | ${{ type: 'old', line: '3', oldLine: '3', newLine: '5' }}
+ ${'with type new'} | ${{ ...thisLine, type: 'new', old_line: 3, new_line: 5 }} | ${{ type: 'new', line: '5', oldLine: '3', newLine: '5' }}
+ `('$desc, sets interop data attributes', ({ line, expectation }) => {
+ createComponent({ line });
+
+ expect(findInteropAttributes(wrapper)).toEqual(expectation);
+ });
+ });
});
diff --git a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js b/spec/frontend/diffs/components/parallel_diff_table_row_spec.js
index dbe8303077d..ed191d849fd 100644
--- a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js
+++ b/spec/frontend/diffs/components/parallel_diff_table_row_spec.js
@@ -5,6 +5,7 @@ import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import { mapParallel } from '~/diffs/components/diff_row_utils';
import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
import { createStore } from '~/mr_notes/stores';
+import { findInteropAttributes } from '../find_interop_attributes';
import discussionsMockData from '../mock_data/diff_discussions';
import diffFileMockData from '../mock_data/diff_file';
@@ -418,5 +419,27 @@ describe('ParallelDiffTableRow', () => {
});
});
});
+
+ describe('interoperability', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('adds old side interoperability data attributes', () => {
+ expect(findInteropAttributes(wrapper, '.line_content.left-side')).toEqual({
+ type: 'old',
+ line: thisLine.left.old_line.toString(),
+ oldLine: thisLine.left.old_line.toString(),
+ });
+ });
+
+ it('adds new side interoperability data attributes', () => {
+ expect(findInteropAttributes(wrapper, '.line_content.right-side')).toEqual({
+ type: 'new',
+ line: thisLine.right.new_line.toString(),
+ newLine: thisLine.right.new_line.toString(),
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/diffs/find_interop_attributes.js b/spec/frontend/diffs/find_interop_attributes.js
new file mode 100644
index 00000000000..d2266b20e16
--- /dev/null
+++ b/spec/frontend/diffs/find_interop_attributes.js
@@ -0,0 +1,20 @@
+export const findInteropAttributes = (parent, sel) => {
+ const target = sel ? parent.find(sel) : parent;
+
+ if (!target.exists()) {
+ return null;
+ }
+
+ const type = target.attributes('data-interop-type');
+
+ if (!type) {
+ return null;
+ }
+
+ return {
+ type,
+ line: target.attributes('data-interop-line'),
+ oldLine: target.attributes('data-interop-old-line'),
+ newLine: target.attributes('data-interop-new-line'),
+ };
+};
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index f46a42fae7a..1861de85ca9 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -17,6 +17,9 @@ import {
fetchDiffFilesBatch,
fetchDiffFilesMeta,
fetchCoverageFiles,
+ clearEtagPoll,
+ stopCodequalityPolling,
+ fetchCodequality,
assignDiscussionsToDiff,
removeDiscussionsFromDiff,
startRenderDiffsQueue,
@@ -98,6 +101,7 @@ describe('DiffsStoreActions', () => {
const endpointMetadata = '/diffs/set/endpoint/metadata';
const endpointBatch = '/diffs/set/endpoint/batch';
const endpointCoverage = '/diffs/set/coverage_reports';
+ const endpointCodequality = '/diffs/set/codequality_diff';
const projectPath = '/root/project';
const dismissEndpoint = '/-/user_callouts';
const showSuggestPopover = false;
@@ -109,6 +113,7 @@ describe('DiffsStoreActions', () => {
endpointBatch,
endpointMetadata,
endpointCoverage,
+ endpointCodequality,
projectPath,
dismissEndpoint,
showSuggestPopover,
@@ -118,6 +123,7 @@ describe('DiffsStoreActions', () => {
endpointBatch: '',
endpointMetadata: '',
endpointCoverage: '',
+ endpointCodequality: '',
projectPath: '',
dismissEndpoint: '',
showSuggestPopover: true,
@@ -130,6 +136,7 @@ describe('DiffsStoreActions', () => {
endpointMetadata,
endpointBatch,
endpointCoverage,
+ endpointCodequality,
projectPath,
dismissEndpoint,
showSuggestPopover,
@@ -299,6 +306,47 @@ describe('DiffsStoreActions', () => {
});
});
+ describe('fetchCodequality', () => {
+ let mock;
+ const endpointCodequality = '/fetch';
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ stopCodequalityPolling();
+ clearEtagPoll();
+ });
+
+ it('should commit SET_CODEQUALITY_DATA with received response', (done) => {
+ const data = {
+ files: { 'app.js': [{ line: 1, description: 'Unexpected alert.', severity: 'minor' }] },
+ };
+
+ mock.onGet(endpointCodequality).reply(200, { data });
+
+ testAction(
+ fetchCodequality,
+ {},
+ { endpointCodequality },
+ [{ type: types.SET_CODEQUALITY_DATA, payload: { data } }],
+ [],
+ done,
+ );
+ });
+
+ it('should show flash on API error', (done) => {
+ mock.onGet(endpointCodequality).reply(400);
+
+ testAction(fetchCodequality, {}, { endpointCodequality }, [], [], () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(expect.stringMatching('Something went wrong'));
+ done();
+ });
+ });
+ });
+
describe('setHighlightedRow', () => {
it('should mark currently selected diff and set lineHash and fileHash of highlightedRow', () => {
testAction(setHighlightedRow, 'ABC_123', {}, [
diff --git a/spec/frontend/diffs/store/getters_spec.js b/spec/frontend/diffs/store/getters_spec.js
index 2e3a66d5b01..aac4909b151 100644
--- a/spec/frontend/diffs/store/getters_spec.js
+++ b/spec/frontend/diffs/store/getters_spec.js
@@ -376,6 +376,26 @@ describe('Diffs Module Getters', () => {
});
});
+ describe('fileCodequalityDiff', () => {
+ beforeEach(() => {
+ Object.assign(localState.codequalityDiff, {
+ files: { 'app.js': [{ line: 1, description: 'Unexpected alert.', severity: 'minor' }] },
+ });
+ });
+
+ it('returns empty array when no codequality data is available', () => {
+ Object.assign(localState.codequalityDiff, {});
+
+ expect(getters.fileCodequalityDiff(localState)('test.js')).toEqual([]);
+ });
+
+ it('returns array when codequality data is available for given file', () => {
+ expect(getters.fileCodequalityDiff(localState)('app.js')).toEqual([
+ { line: 1, description: 'Unexpected alert.', severity: 'minor' },
+ ]);
+ });
+ });
+
describe('suggestionCommitMessage', () => {
let rootState;
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index b549ca42634..eb31724ee45 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -115,6 +115,19 @@ describe('DiffsStoreMutations', () => {
});
});
+ describe('SET_CODEQUALITY_DATA', () => {
+ it('should set codequality data', () => {
+ const state = { codequalityDiff: {} };
+ const codequality = {
+ files: { 'app.js': [{ line: 1, description: 'Unexpected alert.', severity: 'minor' }] },
+ };
+
+ mutations[types.SET_CODEQUALITY_DATA](state, codequality);
+
+ expect(state.codequalityDiff).toEqual(codequality);
+ });
+ });
+
describe('SET_DIFF_VIEW_TYPE', () => {
it('should set diff view type properly', () => {
const state = {};
diff --git a/spec/frontend/diffs/utils/interoperability_spec.js b/spec/frontend/diffs/utils/interoperability_spec.js
new file mode 100644
index 00000000000..2557e83cb4c
--- /dev/null
+++ b/spec/frontend/diffs/utils/interoperability_spec.js
@@ -0,0 +1,67 @@
+import {
+ getInteropInlineAttributes,
+ getInteropNewSideAttributes,
+ getInteropOldSideAttributes,
+ ATTR_TYPE,
+ ATTR_LINE,
+ ATTR_NEW_LINE,
+ ATTR_OLD_LINE,
+} from '~/diffs/utils/interoperability';
+
+describe('~/diffs/utils/interoperability', () => {
+ describe('getInteropInlineAttributes', () => {
+ it.each([
+ ['with null input', { input: null, output: null }],
+ [
+ 'with type=old input',
+ {
+ input: { type: 'old', old_line: 3, new_line: 5 },
+ output: { [ATTR_TYPE]: 'old', [ATTR_LINE]: 3, [ATTR_OLD_LINE]: 3, [ATTR_NEW_LINE]: 5 },
+ },
+ ],
+ [
+ 'with type=old-nonewline input',
+ {
+ input: { type: 'old-nonewline', old_line: 3, new_line: 5 },
+ output: { [ATTR_TYPE]: 'old', [ATTR_LINE]: 3, [ATTR_OLD_LINE]: 3, [ATTR_NEW_LINE]: 5 },
+ },
+ ],
+ [
+ 'with type=new input',
+ {
+ input: { type: 'new', old_line: 3, new_line: 5 },
+ output: { [ATTR_TYPE]: 'new', [ATTR_LINE]: 5, [ATTR_OLD_LINE]: 3, [ATTR_NEW_LINE]: 5 },
+ },
+ ],
+ [
+ 'with type=bogus input',
+ {
+ input: { type: 'bogus', old_line: 3, new_line: 5 },
+ output: { [ATTR_TYPE]: 'new', [ATTR_LINE]: 5, [ATTR_OLD_LINE]: 3, [ATTR_NEW_LINE]: 5 },
+ },
+ ],
+ ])('%s', (desc, { input, output }) => {
+ expect(getInteropInlineAttributes(input)).toEqual(output);
+ });
+ });
+
+ describe('getInteropOldSideAttributes', () => {
+ it.each`
+ input | output
+ ${null} | ${null}
+ ${{ old_line: 2 }} | ${{ [ATTR_TYPE]: 'old', [ATTR_LINE]: 2, [ATTR_OLD_LINE]: 2 }}
+ `('with input=$input', ({ input, output }) => {
+ expect(getInteropOldSideAttributes(input)).toEqual(output);
+ });
+ });
+
+ describe('getInteropNewSideAttributes', () => {
+ it.each`
+ input | output
+ ${null} | ${null}
+ ${{ new_line: 2 }} | ${{ [ATTR_TYPE]: 'new', [ATTR_LINE]: 2, [ATTR_NEW_LINE]: 2 }}
+ `('with input=$input', ({ input, output }) => {
+ expect(getInteropNewSideAttributes(input)).toEqual(output);
+ });
+ });
+});
diff --git a/spec/frontend/fixtures/merge_requests_diffs.rb b/spec/frontend/fixtures/merge_requests_diffs.rb
index 5ad4176f7b8..edf1fcf3c0a 100644
--- a/spec/frontend/fixtures/merge_requests_diffs.rb
+++ b/spec/frontend/fixtures/merge_requests_diffs.rb
@@ -25,6 +25,10 @@ RSpec.describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)'
end
before do
+ # Create a user that matches the project.commit author
+ # This is so that the "author" information will be populated
+ create(:user, email: project.commit.author_email, name: project.commit.author_name)
+
sign_in(user)
end
@@ -33,17 +37,21 @@ RSpec.describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)'
end
it 'merge_request_diffs/with_commit.json' do
- # Create a user that matches the project.commit author
- # This is so that the "author" information will be populated
- create(:user, email: project.commit.author_email, name: project.commit.author_name)
-
render_merge_request(merge_request, commit_id: project.commit.sha)
end
+ it 'merge_request_diffs/diffs_metadata.json' do
+ render_merge_request(merge_request, action: :diffs_metadata)
+ end
+
+ it 'merge_request_diffs/diffs_batch.json' do
+ render_merge_request(merge_request, action: :diffs_batch, page: 1, per_page: 30)
+ end
+
private
- def render_merge_request(merge_request, view: 'inline', **extra_params)
- get :show, params: {
+ def render_merge_request(merge_request, action: :show, view: 'inline', **extra_params)
+ get action, params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.to_param,
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
index cecaa3fc365..8b54a06ac7c 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
@@ -29,21 +29,21 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
class="gl-text-gray-500 gl-mb-2"
data-testid="completion-percentage"
>
- 25% completed
+ 22% completed
@@ -234,6 +234,20 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
+
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
index a78544fb14c..07c7f2df09e 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
@@ -29,21 +29,21 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-gray-500 gl-mb-2"
data-testid="completion-percentage"
>
- 25% completed
+ 22% completed
@@ -94,6 +94,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
@@ -151,6 +152,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
@@ -200,6 +202,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
@@ -249,6 +252,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
@@ -303,6 +307,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
@@ -357,6 +362,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
@@ -422,6 +428,57 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
+
+
+ Create an issue
+
+
+
+ Create/import issues (tickets) to collaborate on ideas and plan work.
+
+
+
+ Create an issue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -487,6 +544,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap
index 831c027cd66..ad8db0822cc 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap
@@ -31,6 +31,10 @@ exports[`Learn GitLab Section Card renders correctly 1`] = `
action="userAdded"
value="[object Object]"
/>
+
{
it('renders the progress percentage', () => {
const text = wrapper.find('[data-testid="completion-percentage"]').text();
- expect(text).toEqual('25% completed');
+ expect(text).toBe('22% completed');
});
it('renders the progress bar with correct values', () => {
- const progressBar = wrapper.find(GlProgressBar);
+ const progressBar = wrapper.findComponent(GlProgressBar);
expect(progressBar.attributes('value')).toBe('2');
- expect(progressBar.attributes('max')).toBe('8');
+ expect(progressBar.attributes('max')).toBe('9');
});
});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js
index fbb989fae32..207944bfa1f 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js
+++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_b_spec.js
@@ -26,13 +26,13 @@ describe('Learn GitLab Design B', () => {
it('renders the progress percentage', () => {
const text = wrapper.find('[data-testid="completion-percentage"]').text();
- expect(text).toEqual('25% completed');
+ expect(text).toBe('22% completed');
});
it('renders the progress bar with correct values', () => {
- const progressBar = wrapper.find(GlProgressBar);
+ const progressBar = wrapper.findComponent(GlProgressBar);
expect(progressBar.attributes('value')).toBe('2');
- expect(progressBar.attributes('max')).toBe('8');
+ expect(progressBar.attributes('max')).toBe('9');
});
});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js b/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js
index caac667e2b1..d6ee2b00c8e 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js
+++ b/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js
@@ -39,4 +39,9 @@ export const testActions = {
completed: false,
svg: 'http://example.com/images/illustration.svg',
},
+ issueCreated: {
+ url: 'http://example.com/',
+ completed: false,
+ svg: 'http://example.com/images/illustration.svg',
+ },
};
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 37fb617ef1a..8c469966be4 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -2,9 +2,15 @@ import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
-import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
+import {
+ IID_FAILURE,
+ LAYER_VIEW,
+ STAGE_VIEW,
+ VIEW_TYPE_KEY,
+} from '~/pipelines/components/graph/constants';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
@@ -21,6 +27,7 @@ const defaultProvide = {
describe('Pipeline graph wrapper', () => {
Vue.use(VueApollo);
+ useLocalStorageSpy();
let wrapper;
const getAlert = () => wrapper.find(GlAlert);
@@ -216,7 +223,7 @@ describe('Pipeline graph wrapper', () => {
});
describe('view dropdown', () => {
- describe('when feature flag is off', () => {
+ describe('when pipelineGraphLayersView feature flag is off', () => {
beforeEach(async () => {
createComponentWithApollo();
jest.runOnlyPendingTimers();
@@ -228,7 +235,7 @@ describe('Pipeline graph wrapper', () => {
});
});
- describe('when feature flag is on', () => {
+ describe('when pipelineGraphLayersView feature flag is on', () => {
let layersFn;
beforeEach(async () => {
layersFn = jest.spyOn(parsingUtils, 'listByLayers');
@@ -245,7 +252,7 @@ describe('Pipeline graph wrapper', () => {
await wrapper.vm.$nextTick();
});
- it('appears', () => {
+ it('appears when pipeline uses needs', () => {
expect(getViewSelector().exists()).toBe(true);
});
@@ -259,6 +266,11 @@ describe('Pipeline graph wrapper', () => {
expect(getStageColumnTitle().text()).toBe('');
});
+ it('saves the view type to local storage', async () => {
+ await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
+ expect(localStorage.setItem.mock.calls).toEqual([[VIEW_TYPE_KEY, LAYER_VIEW]]);
+ });
+
it('calls listByLayers only once no matter how many times view is switched', async () => {
expect(layersFn).not.toHaveBeenCalled();
await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
@@ -269,5 +281,53 @@ describe('Pipeline graph wrapper', () => {
expect(layersFn).toHaveBeenCalledTimes(1);
});
});
+
+ describe('when feature flag is on and local storage is set', () => {
+ beforeEach(async () => {
+ localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW);
+
+ createComponentWithApollo({
+ provide: {
+ glFeatures: {
+ pipelineGraphLayersView: true,
+ },
+ },
+ mountFn: mount,
+ });
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ });
+
+ it('reads the view type from localStorage when available', () => {
+ expect(wrapper.find('[data-testid="pipeline-view-selector"] code').text()).toContain(
+ 'needs:',
+ );
+ });
+ });
+
+ describe('when feature flag is on but pipeline does not use needs', () => {
+ beforeEach(async () => {
+ const nonNeedsResponse = { ...mockPipelineResponse };
+ nonNeedsResponse.data.project.pipeline.usesNeeds = false;
+
+ createComponentWithApollo({
+ provide: {
+ glFeatures: {
+ pipelineGraphLayersView: true,
+ },
+ },
+ mountFn: mount,
+ getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse),
+ });
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ });
+
+ it('does not appear when pipeline does not use needs', () => {
+ expect(getViewSelector().exists()).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index e2d94055205..cf420f68f37 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -8,6 +8,7 @@ export const mockPipelineResponse = {
__typename: 'Pipeline',
id: 163,
iid: '22',
+ usesNeeds: true,
downstream: null,
upstream: null,
stages: {
@@ -569,6 +570,7 @@ export const wrappedPipelineReturn = {
__typename: 'Pipeline',
id: 'gid://gitlab/Ci::Pipeline/175',
iid: '38',
+ usesNeeds: true,
downstream: {
__typename: 'PipelineConnection',
nodes: [],
diff --git a/spec/frontend_integration/diffs/diffs_interopability_api.js b/spec/frontend_integration/diffs/diffs_interopability_api.js
new file mode 100644
index 00000000000..adfb93f27a2
--- /dev/null
+++ b/spec/frontend_integration/diffs/diffs_interopability_api.js
@@ -0,0 +1,25 @@
+/**
+ * This helper module contains the API expectation of the diff output HTML.
+ *
+ * This helps simulate what third-party HTML scrapers, such as Sourcegraph,
+ * should be looking for.
+ */
+export const getDiffCodePart = (codeElement) => {
+ const el = codeElement.closest('[data-interop-type]');
+
+ return el.dataset.interopType === 'old' ? 'base' : 'head';
+};
+
+export const getCodeElementFromLineNumber = (codeView, line, part) => {
+ const type = part === 'base' ? 'old' : 'new';
+
+ const el = codeView.querySelector(`[data-interop-${type}-line="${line}"]`);
+
+ return el ? el.querySelector('span.line') : null;
+};
+
+export const getLineNumberFromCodeElement = (codeElement) => {
+ const el = codeElement.closest('[data-interop-line]');
+
+ return parseInt(el.dataset.interopLine || '', 10);
+};
diff --git a/spec/frontend_integration/diffs/diffs_interopability_spec.js b/spec/frontend_integration/diffs/diffs_interopability_spec.js
new file mode 100644
index 00000000000..cb7659e16d3
--- /dev/null
+++ b/spec/frontend_integration/diffs/diffs_interopability_spec.js
@@ -0,0 +1,161 @@
+import { waitFor } from '@testing-library/dom';
+import { TEST_HOST } from 'helpers/test_constants';
+import initDiffsApp from '~/diffs';
+import { createStore } from '~/mr_notes/stores';
+import {
+ getDiffCodePart,
+ getLineNumberFromCodeElement,
+ getCodeElementFromLineNumber,
+} from './diffs_interopability_api';
+
+jest.mock('~/vue_shared/mixins/gl_feature_flags_mixin', () => () => ({
+ inject: {
+ glFeatures: {
+ from: 'window.gon.features',
+ default: () => global.window.gon?.features,
+ },
+ },
+}));
+
+const TEST_PROJECT_PATH = 'gitlab-org/gitlab-test';
+const TEST_BASE_URL = `/${TEST_PROJECT_PATH}/-/merge_requests/1/`;
+const TEST_DIFF_FILE = 'files/js/commit.coffee';
+const EXPECT_INLINE = [
+ ['head', 1],
+ ['head', 2],
+ ['head', 3],
+ ['base', 4],
+ ['head', 4],
+ null,
+ ['base', 6],
+ ['head', 6],
+ null,
+];
+const EXPECT_PARALLEL_LEFT_SIDE = [
+ ['base', 1],
+ ['base', 2],
+ ['base', 3],
+ ['base', 4],
+ null,
+ ['base', 6],
+ null,
+];
+const EXPECT_PARALLEL_RIGHT_SIDE = [
+ ['head', 1],
+ ['head', 2],
+ ['head', 3],
+ ['head', 4],
+ null,
+ ['head', 6],
+ null,
+];
+
+const startDiffsApp = () => {
+ const el = document.createElement('div');
+ el.id = 'js-diffs-app';
+ document.body.appendChild(el);
+ Object.assign(el.dataset, {
+ endpoint: TEST_BASE_URL,
+ endpointMetadata: `${TEST_BASE_URL}diffs_metadata.json`,
+ endpointBatch: `${TEST_BASE_URL}diffs_batch.json`,
+ projectPath: TEST_PROJECT_PATH,
+ helpPagePath: '/help',
+ currentUserData: 'null',
+ changesEmptyStateIllustration: '',
+ isFluidLayout: 'false',
+ dismissEndpoint: '',
+ showSuggestPopover: 'false',
+ showWhitespaceDefault: 'true',
+ viewDiffsFileByFile: 'false',
+ defaultSuggestionCommitMessage: 'Lorem ipsum',
+ });
+
+ const store = createStore();
+
+ const vm = initDiffsApp(store);
+
+ store.dispatch('setActiveTab', 'diffs');
+
+ return vm;
+};
+
+describe('diffs third party interoperability', () => {
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ document.body.innerHTML = '';
+ });
+
+ const tryOrErrorMessage = (fn) => (...args) => {
+ try {
+ return fn(...args);
+ } catch (e) {
+ return e.message;
+ }
+ };
+
+ const findDiffFile = () => document.querySelector(`.diff-file[data-path="${TEST_DIFF_FILE}"]`);
+ const hasLines = (sel = 'tr.line_holder') => findDiffFile().querySelectorAll(sel).length > 0;
+ const findLineElements = (sel = 'tr.line_holder') =>
+ Array.from(findDiffFile().querySelectorAll(sel));
+
+ const findCodeElements = (lines, sel = 'td.line_content') => {
+ return lines.map((x) => x.querySelector(`${sel} span.line`));
+ };
+
+ const getCodeElementsInteropModel = (codeElements) =>
+ codeElements.map(
+ (x) =>
+ x && [
+ tryOrErrorMessage(getDiffCodePart)(x),
+ tryOrErrorMessage(getLineNumberFromCodeElement)(x),
+ ],
+ );
+
+ describe.each`
+ desc | unifiedDiffComponents | view | rowSelector | codeSelector | expectation
+ ${'inline view'} | ${false} | ${'inline'} | ${'tr.line_holder'} | ${'td.line_content'} | ${EXPECT_INLINE}
+ ${'parallel view left side'} | ${false} | ${'parallel'} | ${'tr.line_holder'} | ${'td.line_content.left-side'} | ${EXPECT_PARALLEL_LEFT_SIDE}
+ ${'parallel view right side'} | ${false} | ${'parallel'} | ${'tr.line_holder'} | ${'td.line_content.right-side'} | ${EXPECT_PARALLEL_RIGHT_SIDE}
+ ${'inline view'} | ${true} | ${'inline'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content'} | ${EXPECT_INLINE}
+ ${'parallel view left side'} | ${true} | ${'parallel'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content.left-side'} | ${EXPECT_PARALLEL_LEFT_SIDE}
+ ${'parallel view right side'} | ${true} | ${'parallel'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content.right-side'} | ${EXPECT_PARALLEL_RIGHT_SIDE}
+ `(
+ '$desc (unifiedDiffComponents=$unifiedDiffComponents)',
+ ({ unifiedDiffComponents, view, rowSelector, codeSelector, expectation }) => {
+ beforeEach(async () => {
+ global.jsdom.reconfigure({
+ url: `${TEST_HOST}/${TEST_BASE_URL}/diffs?view=${view}`,
+ });
+ window.gon.features = { unifiedDiffComponents };
+
+ vm = startDiffsApp();
+
+ await waitFor(() => expect(hasLines(rowSelector)).toBe(true));
+ });
+
+ it('should match diff model', () => {
+ const lines = findLineElements(rowSelector);
+ const codes = findCodeElements(lines, codeSelector);
+
+ expect(getCodeElementsInteropModel(codes)).toEqual(expectation);
+ });
+
+ it.each`
+ lineNumber | part | expectedText
+ ${4} | ${'base'} | ${'new CommitFile(this)'}
+ ${4} | ${'head'} | ${'new CommitFile(@)'}
+ ${2} | ${'base'} | ${'constructor: ->'}
+ ${2} | ${'head'} | ${'constructor: ->'}
+ `(
+ 'should find code element lineNumber=$lineNumber part=$part',
+ ({ lineNumber, part, expectedText }) => {
+ const codeElement = getCodeElementFromLineNumber(findDiffFile(), lineNumber, part);
+
+ expect(codeElement.textContent.trim()).toBe(expectedText);
+ },
+ );
+ },
+ );
+});
diff --git a/spec/frontend_integration/test_helpers/fixtures.js b/spec/frontend_integration/test_helpers/fixtures.js
index b2768440607..5673e36197f 100644
--- a/spec/frontend_integration/test_helpers/fixtures.js
+++ b/spec/frontend_integration/test_helpers/fixtures.js
@@ -40,6 +40,12 @@ export const getMergeRequestVersions = factory.json(() =>
export const getRepositoryFiles = factory.json(() =>
require('test_fixtures/projects_json/files.json'),
);
+export const getDiffsMetadata = factory.json(() =>
+ require('test_fixtures/merge_request_diffs/diffs_metadata.json'),
+);
+export const getDiffsBatch = factory.json(() =>
+ require('test_fixtures/merge_request_diffs/diffs_batch.json'),
+);
export const getPipelinesEmptyResponse = factory.json(() =>
require('test_fixtures/projects_json/pipelines_empty.json'),
);
diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/diffs.js b/spec/frontend_integration/test_helpers/mock_server/routes/diffs.js
new file mode 100644
index 00000000000..8301627e842
--- /dev/null
+++ b/spec/frontend_integration/test_helpers/mock_server/routes/diffs.js
@@ -0,0 +1,22 @@
+import { getDiffsMetadata, getDiffsBatch } from 'test_helpers/fixtures';
+import { withValues } from 'test_helpers/utils/obj';
+
+export default (server) => {
+ server.get('/:namespace/:project/-/merge_requests/:mrid/diffs_metadata.json', () => {
+ return getDiffsMetadata();
+ });
+
+ server.get('/:namespace/:project/-/merge_requests/:mrid/diffs_batch.json', () => {
+ const { pagination, ...result } = getDiffsBatch();
+
+ return {
+ ...result,
+ pagination: withValues(pagination, {
+ current_page: null,
+ next_page: null,
+ total_pages: 1,
+ next_page_href: null,
+ }),
+ };
+ });
+};
diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/index.js b/spec/frontend_integration/test_helpers/mock_server/routes/index.js
index e30fecf2f06..48eff2702dd 100644
--- a/spec/frontend_integration/test_helpers/mock_server/routes/index.js
+++ b/spec/frontend_integration/test_helpers/mock_server/routes/index.js
@@ -5,6 +5,7 @@ export default (server) => {
require('./projects'),
require('./repository'),
require('./ci'),
+ require('./diffs'),
require('./404'),
].forEach(({ default: setup }) => {
setup(server);
diff --git a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
index 5eda840854a..6ffc8b045e9 100644
--- a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
@@ -39,6 +39,24 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
expect(result).to match_array([issue1])
end
+
+ it 'raises an exception if both assignee_username and assignee_wildcard_id are present' do
+ expect do
+ resolve_board_list_issues(args: { filters: { assignee_username: ['username'], assignee_wildcard_id: 'NONE' } })
+ end.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ end
+
+ it 'accepts assignee wildcard id NONE' do
+ result = resolve_board_list_issues(args: { filters: { assignee_wildcard_id: 'NONE' } })
+
+ expect(result).to match_array([issue1, issue2, issue3])
+ end
+
+ it 'accepts assignee wildcard id ANY' do
+ result = resolve_board_list_issues(args: { filters: { assignee_wildcard_id: 'ANY' } })
+
+ expect(result).to match_array([])
+ end
end
end
diff --git a/spec/graphql/types/boards/board_issue_input_type_spec.rb b/spec/graphql/types/boards/board_issue_input_type_spec.rb
index 6319ff9a88e..5d3efb9b40d 100644
--- a/spec/graphql/types/boards/board_issue_input_type_spec.rb
+++ b/spec/graphql/types/boards/board_issue_input_type_spec.rb
@@ -5,9 +5,9 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['BoardIssueInput'] do
it { expect(described_class.graphql_name).to eq('BoardIssueInput') }
- it 'exposes negated issue arguments' do
+ it 'has specific fields' do
allowed_args = %w(labelName milestoneTitle assigneeUsername authorUsername
- releaseTag myReactionEmoji not search)
+ releaseTag myReactionEmoji not search assigneeWildcardId)
expect(described_class.arguments.keys).to include(*allowed_args)
expect(described_class.arguments['not'].type).to eq(Types::Boards::NegatedBoardIssueInputType)
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 3ccf5ded9f5..ae039c1a8b1 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -168,11 +168,13 @@ RSpec.describe ApplicationHelper do
it { expect(helper.active_when(false)).to eq(nil) }
end
- describe '#promo_host' do
- subject { helper.promo_host }
+ unless Gitlab.jh?
+ describe '#promo_host' do
+ subject { helper.promo_host }
- it 'returns the url' do
- is_expected.to eq('about.gitlab.com')
+ it 'returns the url' do
+ is_expected.to eq('about.gitlab.com')
+ end
end
end
@@ -180,7 +182,7 @@ RSpec.describe ApplicationHelper do
subject { helper.promo_url }
it 'returns the url' do
- is_expected.to eq('https://about.gitlab.com')
+ is_expected.to eq("https://#{helper.promo_host}")
end
it 'changes if promo_host changes' do
@@ -194,7 +196,7 @@ RSpec.describe ApplicationHelper do
subject { helper.contact_sales_url }
it 'returns the url' do
- is_expected.to eq('https://about.gitlab.com/sales')
+ is_expected.to eq("https://#{helper.promo_host}/sales")
end
it 'changes if promo_url changes' do
diff --git a/spec/helpers/learn_gitlab_helper_spec.rb b/spec/helpers/learn_gitlab_helper_spec.rb
index 6cee8a9191c..82c8e4ba596 100644
--- a/spec/helpers/learn_gitlab_helper_spec.rb
+++ b/spec/helpers/learn_gitlab_helper_spec.rb
@@ -27,6 +27,7 @@ RSpec.describe LearnGitlabHelper do
it 'has all actions' do
expect(onboarding_actions_data.keys).to contain_exactly(
+ :issue_created,
:git_write,
:pipeline_created,
:merge_request_created,
diff --git a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
index 15eebf62a39..9c3bc935acc 100644
--- a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::GitalyClient::ObjectPoolService do
subject { described_class.new(object_pool) }
before do
- subject.create(raw_repository)
+ subject.create(raw_repository) # rubocop:disable Rails/SaveBang
end
describe '#create' do
@@ -22,7 +22,7 @@ RSpec.describe Gitlab::GitalyClient::ObjectPoolService do
context 'when the pool already exists' do
it 'returns an error' do
expect do
- subject.create(raw_repository)
+ subject.create(raw_repository) # rubocop:disable Rails/SaveBang
end.to raise_error(GRPC::FailedPrecondition)
end
end
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index 5954e199f3c..26ec194a2e7 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -270,7 +270,7 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
let(:object_pool_service) { Gitlab::GitalyClient::ObjectPoolService.new(object_pool) }
before do
- object_pool_service.create(repository)
+ object_pool_service.create(repository) # rubocop:disable Rails/SaveBang
object_pool_service.link_repository(repository)
end
diff --git a/spec/lib/gitlab/subscription_portal_spec.rb b/spec/lib/gitlab/subscription_portal_spec.rb
index 351af3c07d2..ad1affdac0b 100644
--- a/spec/lib/gitlab/subscription_portal_spec.rb
+++ b/spec/lib/gitlab/subscription_portal_spec.rb
@@ -3,39 +3,41 @@
require 'spec_helper'
RSpec.describe ::Gitlab::SubscriptionPortal do
- describe '.default_subscriptions_url' do
- subject { described_class.default_subscriptions_url }
+ unless Gitlab.jh?
+ describe '.default_subscriptions_url' do
+ subject { described_class.default_subscriptions_url }
- context 'on non test and non dev environments' do
- before do
- allow(Rails).to receive_message_chain(:env, :test?).and_return(false)
- allow(Rails).to receive_message_chain(:env, :development?).and_return(false)
+ context 'on non test and non dev environments' do
+ before do
+ allow(Rails).to receive_message_chain(:env, :test?).and_return(false)
+ allow(Rails).to receive_message_chain(:env, :development?).and_return(false)
+ end
+
+ it 'returns production subscriptions app URL' do
+ is_expected.to eq('https://customers.gitlab.com')
+ end
end
- it 'returns production subscriptions app URL' do
- is_expected.to eq('https://customers.gitlab.com')
- end
- end
+ context 'on dev environment' do
+ before do
+ allow(Rails).to receive_message_chain(:env, :test?).and_return(false)
+ allow(Rails).to receive_message_chain(:env, :development?).and_return(true)
+ end
- context 'on dev environment' do
- before do
- allow(Rails).to receive_message_chain(:env, :test?).and_return(false)
- allow(Rails).to receive_message_chain(:env, :development?).and_return(true)
+ it 'returns staging subscriptions app url' do
+ is_expected.to eq('https://customers.stg.gitlab.com')
+ end
end
- it 'returns staging subscriptions app url' do
- is_expected.to eq('https://customers.stg.gitlab.com')
- end
- end
+ context 'on test environment' do
+ before do
+ allow(Rails).to receive_message_chain(:env, :test?).and_return(true)
+ allow(Rails).to receive_message_chain(:env, :development?).and_return(false)
+ end
- context 'on test environment' do
- before do
- allow(Rails).to receive_message_chain(:env, :test?).and_return(true)
- allow(Rails).to receive_message_chain(:env, :development?).and_return(false)
- end
-
- it 'returns staging subscriptions app url' do
- is_expected.to eq('https://customers.stg.gitlab.com')
+ it 'returns staging subscriptions app url' do
+ is_expected.to eq('https://customers.stg.gitlab.com')
+ end
end
end
end
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index c5738ae730f..4df00eaa439 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -247,75 +247,117 @@ RSpec.describe Gitlab do
end
end
- describe '.ee?' do
+ describe 'ee? and jh?' do
before do
- stub_env('FOSS_ONLY', nil) # Make sure the ENV is clean
+ # Make sure the ENV is clean
+ stub_env('FOSS_ONLY', nil)
+ stub_env('EE_ONLY', nil)
+
described_class.instance_variable_set(:@is_ee, nil)
+ described_class.instance_variable_set(:@is_jh, nil)
end
after do
described_class.instance_variable_set(:@is_ee, nil)
+ described_class.instance_variable_set(:@is_jh, nil)
end
- context 'for EE' do
- before do
- root = Pathname.new('dummy')
- license_path = double(:path, exist?: true)
+ def stub_path(*paths, **arguments)
+ root = Pathname.new('dummy')
+ pathname = double(:path, **arguments)
- allow(described_class)
- .to receive(:root)
- .and_return(root)
+ allow(described_class)
+ .to receive(:root)
+ .and_return(root)
+ allow(root).to receive(:join)
+
+ paths.each do |path|
allow(root)
.to receive(:join)
- .with('ee/app/models/license.rb')
- .and_return(license_path)
+ .with(path)
+ .and_return(pathname)
+ end
+ end
+
+ describe '.ee?' do
+ context 'for EE' do
+ before do
+ stub_path('ee/app/models/license.rb', exist?: true)
+ end
+
+ context 'when using FOSS_ONLY=1' do
+ before do
+ stub_env('FOSS_ONLY', '1')
+ end
+
+ it 'returns not to be EE' do
+ expect(described_class).not_to be_ee
+ end
+ end
+
+ context 'when using FOSS_ONLY=0' do
+ before do
+ stub_env('FOSS_ONLY', '0')
+ end
+
+ it 'returns to be EE' do
+ expect(described_class).to be_ee
+ end
+ end
+
+ context 'when using default FOSS_ONLY' do
+ it 'returns to be EE' do
+ expect(described_class).to be_ee
+ end
+ end
end
- context 'when using FOSS_ONLY=1' do
+ context 'for CE' do
before do
- stub_env('FOSS_ONLY', '1')
+ stub_path('ee/app/models/license.rb', exist?: false)
end
it 'returns not to be EE' do
expect(described_class).not_to be_ee
end
end
-
- context 'when using FOSS_ONLY=0' do
- before do
- stub_env('FOSS_ONLY', '0')
- end
-
- it 'returns to be EE' do
- expect(described_class).to be_ee
- end
- end
-
- context 'when using default FOSS_ONLY' do
- it 'returns to be EE' do
- expect(described_class).to be_ee
- end
- end
end
- context 'for CE' do
- before do
- root = double(:path)
- license_path = double(:path, exists?: false)
+ describe '.jh?' do
+ context 'for JH' do
+ before do
+ stub_path(
+ 'ee/app/models/license.rb',
+ 'jh',
+ exist?: true)
+ end
- allow(described_class)
- .to receive(:root)
- .and_return(Pathname.new('dummy'))
+ context 'when using default FOSS_ONLY and EE_ONLY' do
+ it 'returns to be JH' do
+ expect(described_class).to be_jh
+ end
+ end
- allow(root)
- .to receive(:join)
- .with('ee/app/models/license.rb')
- .and_return(license_path)
- end
+ context 'when using FOSS_ONLY=1' do
+ before do
+ stub_env('FOSS_ONLY', '1')
+ end
- it 'returns not to be EE' do
- expect(described_class).not_to be_ee
+ it 'returns not to be JH' do
+ expect(described_class).not_to be_jh
+ end
+ end
+
+ context 'when using EE_ONLY=1' do
+ before do
+ stub_env('EE_ONLY', '1')
+ end
+
+ it 'returns not to be JH' do
+ expect(described_class).not_to be_jh
+ end
+ end
end
end
end
diff --git a/spec/migrations/20210413132500_reschedule_artifact_expiry_backfill_again_spec.rb b/spec/migrations/20210413132500_reschedule_artifact_expiry_backfill_again_spec.rb
new file mode 100644
index 00000000000..4f36a95f9cf
--- /dev/null
+++ b/spec/migrations/20210413132500_reschedule_artifact_expiry_backfill_again_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require Rails.root.join('db', 'post_migrate', '20210413132500_reschedule_artifact_expiry_backfill_again.rb')
+
+RSpec.describe RescheduleArtifactExpiryBackfillAgain, :migration do
+ let(:migration_class) { Gitlab::BackgroundMigration::BackfillArtifactExpiryDate }
+ let(:migration_name) { migration_class.to_s.demodulize }
+
+ before do
+ table(:namespaces).create!(id: 123, name: 'test_namespace', path: 'test_namespace')
+ table(:projects).create!(id: 123, name: 'sample_project', path: 'sample_project', namespace_id: 123)
+ end
+
+ it 'correctly schedules background migrations' do
+ first_artifact = create_artifact(job_id: 0, expire_at: nil, created_at: Date.new(2020, 06, 21))
+ second_artifact = create_artifact(job_id: 1, expire_at: nil, created_at: Date.new(2020, 06, 21))
+ create_artifact(job_id: 2, expire_at: Date.yesterday, created_at: Date.new(2020, 06, 21))
+ create_artifact(job_id: 3, expire_at: nil, created_at: Date.new(2020, 06, 23))
+
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ expect(migration_name).to be_scheduled_migration_with_multiple_args(first_artifact.id, second_artifact.id)
+ end
+ end
+ end
+
+ private
+
+ def create_artifact(params)
+ table(:ci_builds).create!(id: params[:job_id], project_id: 123)
+ table(:ci_job_artifacts).create!(project_id: 123, file_type: 1, **params)
+ end
+end
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 4ef5ab7af48..010b7455f85 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -200,4 +200,18 @@ RSpec.describe NotificationSetting do
subject.email_events
end
end
+
+ describe '#order_by_id_asc' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:other_project) { create(:project) }
+ let_it_be(:notification_setting_1) { create(:notification_setting, project: project) }
+ let_it_be(:notification_setting_2) { create(:notification_setting, project: other_project) }
+ let_it_be(:notification_setting_3) { create(:notification_setting, project: project) }
+
+ let(:ids) { [notification_setting_1, notification_setting_2, notification_setting_3].map(&:id) }
+
+ subject(:ordered_records) { described_class.where(id: ids, source: project).order_by_id_asc }
+
+ it { is_expected.to eq([notification_setting_1, notification_setting_3]) }
+ end
end
diff --git a/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb b/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
new file mode 100644
index 00000000000..16e699b7e0e
--- /dev/null
+++ b/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Preloaders::UserMaxAccessLevelInProjectsPreloader do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project_1) { create(:project) }
+ let_it_be(:project_2) { create(:project) }
+ let_it_be(:project_3) { create(:project) }
+
+ let(:projects) { [project_1, project_2, project_3] }
+
+ before do
+ project_1.add_developer(user)
+ project_2.add_developer(user)
+ end
+
+ context 'preload maximum access level to avoid querying project_authorizations', :request_store do
+ it 'avoids N+1 queries', :request_store do
+ Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, user).execute
+
+ query_count = ActiveRecord::QueryRecorder.new do
+ projects.each { |project| user.can?(:read_project, project) }
+ end.count
+
+ expect(query_count).to eq(0)
+ end
+
+ it 'runs N queries without preloading' do
+ query_count = ActiveRecord::QueryRecorder.new do
+ projects.each { |project| user.can?(:read_project, project) }
+ end.count
+
+ expect(query_count).to eq(projects.size)
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 098c88147c3..55d049b9d47 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -5225,57 +5225,27 @@ RSpec.describe Project, factory_default: :keep do
end
describe '#default_branch' do
- context 'with an empty repository' do
- let_it_be(:project) { create(:project_empty_repo) }
+ context 'with default_branch_name' do
+ let_it_be_with_refind(:root_group) { create(:group) }
+ let_it_be_with_refind(:project_group) { create(:group, parent: root_group) }
+ let_it_be_with_refind(:project) { create(:project, path: 'avatar', namespace: project_group) }
- context 'group.default_branch_name is available' do
- let(:project_group) { create(:group) }
- let(:project) { create(:project, path: 'avatar', namespace: project_group) }
-
- before do
- expect(Gitlab::CurrentSettings)
- .not_to receive(:default_branch_name)
-
- expect(project.group)
- .to receive(:default_branch_name)
- .and_return('example_branch')
- end
-
- it 'returns the group default value' do
- expect(project.default_branch).to eq('example_branch')
- end
+ where(:instance_branch, :root_group_branch, :project_group_branch, :project_branch) do
+ '' | nil | nil | nil
+ nil | nil | nil | nil
+ 'main' | nil | nil | 'main'
+ 'main' | 'root_branch' | nil | 'root_branch'
+ 'main' | 'root_branch' | 'group_branch' | 'group_branch'
end
- context 'Gitlab::CurrentSettings.default_branch_name is available' do
+ with_them do
before do
- expect(Gitlab::CurrentSettings)
- .to receive(:default_branch_name)
- .and_return(example_branch_name)
+ allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return(instance_branch)
+ root_group.namespace_settings.update!(default_branch_name: root_group_branch)
+ project_group.namespace_settings.update!(default_branch_name: project_group_branch)
end
- context 'is missing or nil' do
- let(:example_branch_name) { nil }
-
- it "returns nil" do
- expect(project.default_branch).to be_nil
- end
- end
-
- context 'is blank' do
- let(:example_branch_name) { '' }
-
- it 'returns nil' do
- expect(project.default_branch).to be_nil
- end
- end
-
- context 'is present' do
- let(:example_branch_name) { 'example_branch_name' }
-
- it 'returns the expected branch name' do
- expect(project.default_branch).to eq(example_branch_name)
- end
- end
+ it { expect(project.default_branch).to eq(project_branch) }
end
end
end
diff --git a/spec/services/boards/lists/update_service_spec.rb b/spec/services/boards/lists/update_service_spec.rb
index cdc7784469a..10fed9b7aac 100644
--- a/spec/services/boards/lists/update_service_spec.rb
+++ b/spec/services/boards/lists/update_service_spec.rb
@@ -6,47 +6,6 @@ RSpec.describe Boards::Lists::UpdateService do
let(:user) { create(:user) }
let!(:list) { create(:list, board: board, position: 0) }
- shared_examples 'moving list' do
- context 'when user can admin list' do
- it 'calls Lists::MoveService to update list position' do
- board.resource_parent.add_developer(user)
-
- expect(Boards::Lists::MoveService).to receive(:new).with(board.resource_parent, user, params).and_call_original
- expect_any_instance_of(Boards::Lists::MoveService).to receive(:execute).with(list)
-
- service.execute(list)
- end
- end
-
- context 'when user cannot admin list' do
- it 'does not call Lists::MoveService to update list position' do
- expect(Boards::Lists::MoveService).not_to receive(:new)
-
- service.execute(list)
- end
- end
- end
-
- shared_examples 'updating list preferences' do
- context 'when user can read list' do
- it 'updates list preference for user' do
- board.resource_parent.add_guest(user)
-
- service.execute(list)
-
- expect(list.preferences_for(user).collapsed).to eq(true)
- end
- end
-
- context 'when user cannot read list' do
- it 'does not update list preference for user' do
- service.execute(list)
-
- expect(list.preferences_for(user).collapsed).to be_nil
- end
- end
- end
-
describe '#execute' do
let(:service) { described_class.new(board.resource_parent, user, params) }
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 487c7bb10e2..2607f2e8e21 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -55,6 +55,7 @@ require 'rainbow/ext/string'
Rainbow.enabled = false
require_relative('../ee/spec/spec_helper') if Gitlab.ee?
+require_relative('../jh/spec/spec_helper') if Gitlab.jh?
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
diff --git a/spec/support/shared_examples/boards/lists/update_service_shared_examples.rb b/spec/support/shared_examples/boards/lists/update_service_shared_examples.rb
new file mode 100644
index 00000000000..d8a74f2582d
--- /dev/null
+++ b/spec/support/shared_examples/boards/lists/update_service_shared_examples.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'moving list' do
+ context 'when user can admin list' do
+ it 'calls Lists::MoveService to update list position' do
+ board.resource_parent.add_developer(user)
+
+ expect_next_instance_of(Boards::Lists::MoveService, board.resource_parent, user, params) do |move_service|
+ expect(move_service).to receive(:execute).with(list).and_call_original
+ end
+
+ service.execute(list)
+ end
+ end
+
+ context 'when user cannot admin list' do
+ it 'does not call Lists::MoveService to update list position' do
+ expect(Boards::Lists::MoveService).not_to receive(:new)
+
+ service.execute(list)
+ end
+ end
+end
+
+RSpec.shared_examples 'updating list preferences' do
+ context 'when user can read list' do
+ it 'updates list preference for user' do
+ board.resource_parent.add_guest(user)
+
+ service.execute(list)
+
+ expect(list.preferences_for(user).collapsed).to eq(true)
+ end
+ end
+
+ context 'when user cannot read list' do
+ it 'does not update list preference for user' do
+ service.execute(list)
+
+ expect(list.preferences_for(user).collapsed).to be_falsy
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index d704d66184c..9a9fc0f692e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -907,10 +907,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
-"@gitlab/ui@29.3.0":
- version "29.3.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-29.3.0.tgz#e549b73341246bb9cd1005b2f4c10a599c680cb1"
- integrity sha512-TWc3O3w7L+aCLC7Vp2JbYTFgCwseLExxjhwDfJuc2Iwkr+1k8k1ygctbid9XRX2Jcy3JPbL+o+m0K/ZXMJTWdg==
+"@gitlab/ui@29.4.0":
+ version "29.4.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-29.4.0.tgz#601b13700c5af4eeb6ca60fe72e59a2ee1b3d19a"
+ integrity sha512-GdV1DIP0Oq/abzlh93FPpJX/kb1Swk81znXEYpzhoL7vrg4Lotbkhlz+oaNMZF/F/YOZ8QUCKTcs2Q7HGgkKmQ==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"
@@ -12429,10 +12429,10 @@ webidl-conversions@^6.1.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
-webpack-bundle-analyzer@^4.4.0:
- version "4.4.0"
- resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.0.tgz#74013106e7e2b07cbd64f3a5ae847f7e814802c7"
- integrity sha512-9DhNa+aXpqdHk8LkLPTBU/dMfl84Y+WE2+KnfI6rSpNRNVKa0VGLjPd2pjFubDeqnWmulFggxmWBxhfJXZnR0g==
+webpack-bundle-analyzer@^4.4.1:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.1.tgz#c71fb2eaffc10a4754d7303b224adb2342069da1"
+ integrity sha512-j5m7WgytCkiVBoOGavzNokBOqxe6Mma13X1asfVYtKWM3wxBiRRu1u1iG0Iol5+qp9WgyhkMmBAcvjEfJ2bdDw==
dependencies:
acorn "^8.0.4"
acorn-walk "^8.0.0"
|