diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md
index 86083c565fb..39f4de94553 100644
--- a/.gitlab/issue_templates/Feature Flag Roll Out.md
+++ b/.gitlab/issue_templates/Feature Flag Roll Out.md
@@ -75,7 +75,10 @@ Are there any other stages or teams involved that need to be kept in the loop?
issue](https://about.gitlab.com/handbook/engineering/infrastructure/change-management/#feature-flags-and-the-change-management-process). Cross
link the issue here if it does.
+- [ ] Ensure that you or a representative in development can be available for at least 2 hours after feature flag updates in production. If a different developer will be covering, or an exception is needed, please inform the oncall SRE by using the `@sre-oncall` Slack alias.
+
*Partial Rollout Phase*
+
- [ ] Enable on GitLab.com for individual groups/projects listed above and verify behaviour (`/chatops run feature set --project=gitlab-org/gitlab feature_name true`)
- [ ] Verify behaviour (See Beta Groups) and add details with screenshots as a comment on this issue
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index c1285c77e01..eba11804513 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -1998,6 +1998,8 @@ Gitlab/NamespacedClass:
- 'app/serializers/group_entity.rb'
- 'app/serializers/group_group_link_entity.rb'
- 'app/serializers/group_group_link_serializer.rb'
+ - 'app/serializers/group_issuable_autocomplete_entity.rb'
+ - 'app/serializers/group_issuable_autocomplete_serializer.rb'
- 'app/serializers/group_serializer.rb'
- 'app/serializers/issuable_entity.rb'
- 'app/serializers/issuable_sidebar_basic_entity.rb'
@@ -2500,8 +2502,6 @@ Gitlab/NamespacedClass:
- 'ee/app/serializers/geo_project_registry_entity.rb'
- 'ee/app/serializers/geo_project_registry_serializer.rb'
- 'ee/app/serializers/group_analytics_serializer.rb'
- - 'ee/app/serializers/group_issuable_autocomplete_entity.rb'
- - 'ee/app/serializers/group_issuable_autocomplete_serializer.rb'
- 'ee/app/serializers/group_vulnerability_autocomplete_entity.rb'
- 'ee/app/serializers/group_vulnerability_autocomplete_serializer.rb'
- 'ee/app/serializers/invited_group_entity.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index b15a3879a77..e70d1d0105b 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1481a9195c200e375a177cf201058b88bebe271b
+c67f1a2bb56d8fa3403b529fd3bf36dba3a6488c
diff --git a/Gemfile b/Gemfile
index 9cd928d5fd8..3504041ec6c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -481,7 +481,7 @@ end
gem 'spamcheck', '~> 0.0.5'
# Gitaly GRPC protocol definitions
-gem 'gitaly', '~> 13.11.0.pre.rc1'
+gem 'gitaly', '~> 13.12.0.pre.rc1'
gem 'grpc', '~> 1.30.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index 8111ff597e7..643cb7046c9 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -439,7 +439,7 @@ GEM
rails (>= 3.2.0)
git (1.7.0)
rchardet (~> 1.8)
- gitaly (13.11.0.pre.rc1)
+ gitaly (13.12.0.pre.rc1)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab (4.16.1)
@@ -1446,7 +1446,7 @@ DEPENDENCIES
gettext (~> 3.3)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly (~> 13.11.0.pre.rc1)
+ gitaly (~> 13.12.0.pre.rc1)
github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5)
gitlab-dangerfiles (~> 2.0.0)
diff --git a/app/views/shared/icons/_dev_ops_report_no_data.svg b/app/assets/images/dev_ops_report_no_data.svg
similarity index 100%
rename from app/views/shared/icons/_dev_ops_report_no_data.svg
rename to app/assets/images/dev_ops_report_no_data.svg
diff --git a/app/assets/javascripts/analytics/devops_report/components/devops_score.vue b/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
index 7dc552623f6..1a3289ffb75 100644
--- a/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
+++ b/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
@@ -1,5 +1,5 @@
-
+
+
+
{{ __('It may be several days before you see feature usage data.') }}
+ {{
+ __('See example DevOps Score page in our documentation.')
+ }}
+
+
+
{{ titleHelperText }}
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score.js b/app/assets/javascripts/analytics/devops_report/devops_score.js
index 46b27e0a340..18f7cf0c3ab 100644
--- a/app/assets/javascripts/analytics/devops_report/devops_score.js
+++ b/app/assets/javascripts/analytics/devops_report/devops_score.js
@@ -6,12 +6,14 @@ export default () => {
if (!el) return false;
- const { devopsScoreMetrics } = el.dataset;
+ const { devopsScoreMetrics, devopsReportDocsPath, noDataImagePath } = el.dataset;
return new Vue({
el,
provide: {
devopsScoreMetrics: JSON.parse(devopsScoreMetrics),
+ devopsReportDocsPath,
+ noDataImagePath,
},
render(h) {
return h(DevopsScore);
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score_empty_state.js b/app/assets/javascripts/analytics/devops_report/devops_score_disabled_usage_ping.js
similarity index 96%
rename from app/assets/javascripts/analytics/devops_report/devops_score_empty_state.js
rename to app/assets/javascripts/analytics/devops_report/devops_score_disabled_usage_ping.js
index 0cb8d9be0e4..0131407e723 100644
--- a/app/assets/javascripts/analytics/devops_report/devops_score_empty_state.js
+++ b/app/assets/javascripts/analytics/devops_report/devops_score_disabled_usage_ping.js
@@ -6,7 +6,7 @@ export default () => {
// eslint-disable-next-line no-new
new UserCallout();
- const emptyStateContainer = document.getElementById('js-devops-empty-state');
+ const emptyStateContainer = document.getElementById('js-devops-usage-ping-disabled');
if (!emptyStateContainer) return false;
diff --git a/app/assets/javascripts/pages/admin/dev_ops_report/index.js b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
index 23b53315680..d6fa1be29b0 100644
--- a/app/assets/javascripts/pages/admin/dev_ops_report/index.js
+++ b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
@@ -1,5 +1,5 @@
import initDevOpsScore from '~/analytics/devops_report/devops_score';
-import initDevOpsScoreEmptyState from '~/analytics/devops_report/devops_score_empty_state';
+import initDevOpsScoreDisabledUsagePing from '~/analytics/devops_report/devops_score_disabled_usage_ping';
-initDevOpsScoreEmptyState();
+initDevOpsScoreDisabledUsagePing();
initDevOpsScore();
diff --git a/app/assets/javascripts/pages/groups/milestones/edit/index.js b/app/assets/javascripts/pages/groups/milestones/edit/index.js
index af0264c7992..364b0d95d9c 100644
--- a/app/assets/javascripts/pages/groups/milestones/edit/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/edit/index.js
@@ -1,3 +1,3 @@
import initForm from '../../../../shared/milestones/form';
-initForm(false);
+initForm();
diff --git a/app/assets/javascripts/pages/groups/milestones/new/index.js b/app/assets/javascripts/pages/groups/milestones/new/index.js
index af0264c7992..364b0d95d9c 100644
--- a/app/assets/javascripts/pages/groups/milestones/new/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/new/index.js
@@ -1,3 +1,3 @@
import initForm from '../../../../shared/milestones/form';
-initForm(false);
+initForm();
diff --git a/app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js b/app/assets/javascripts/pages/groups/settings/packages_and_registries/show/index.js
similarity index 100%
rename from app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js
rename to app/assets/javascripts/pages/groups/settings/packages_and_registries/show/index.js
diff --git a/app/assets/javascripts/pages/projects/settings/packages_and_registries/index/index.js b/app/assets/javascripts/pages/projects/settings/packages_and_registries/show/index.js
similarity index 100%
rename from app/assets/javascripts/pages/projects/settings/packages_and_registries/index/index.js
rename to app/assets/javascripts/pages/projects/settings/packages_and_registries/show/index.js
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index 329964d009a..c6ce29acb09 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -28,7 +28,7 @@ export default {
{{
- s__('mrWidget|Before this can be merged, one or more threads must be resolved.')
+ s__('mrWidget|Merge blocked: all threads must be resolved.')
}} (group) do
- joins(:issue).where(
- 'EXISTS (?)',
- Project.select(1).where(namespace: group.self_and_descendants)
- .where('issues.project_id = projects.id')
- )
+ scope :in_group, -> (group) do
+ joins(:project).where(projects: { namespace: group.self_and_descendants })
end
scope :between_times, -> (start_time, end_time) do
diff --git a/app/policies/ci/stage_policy.rb b/app/policies/ci/stage_policy.rb
new file mode 100644
index 00000000000..1e774df9f58
--- /dev/null
+++ b/app/policies/ci/stage_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Ci
+ class StagePolicy < BasePolicy
+ delegate :pipeline
+ end
+end
diff --git a/app/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter.rb b/app/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter.rb
index 2fe3104fe69..d28b4523fd5 100644
--- a/app/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter.rb
+++ b/app/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter.rb
@@ -5,18 +5,20 @@ module Ci
class CodeQualityMrDiffPresenter < Gitlab::View::Presenter::Delegated
include Gitlab::Utils::StrongMemoize
- def for_files(filenames)
- quality_files = raw_report["files"].select { |key| filenames.include?(key) }
+ def for_files(merge_request)
+ filenames = merge_request.new_paths
+ mr_diff_report = raw_report(merge_request.id)
+ quality_files = mr_diff_report["files"]&.select { |key| filenames.include?(key) }
{ files: quality_files }
end
private
- def raw_report
+ def raw_report(merge_request_id)
strong_memoize(:raw_report) do
self.each_blob do |blob|
- Gitlab::Json.parse(blob).with_indifferent_access
+ Gitlab::Json.parse(blob).with_indifferent_access.fetch("merge_request_#{merge_request_id}", {})
end
end
end
diff --git a/app/serializers/group_issuable_autocomplete_entity.rb b/app/serializers/group_issuable_autocomplete_entity.rb
new file mode 100644
index 00000000000..f950a7db785
--- /dev/null
+++ b/app/serializers/group_issuable_autocomplete_entity.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class GroupIssuableAutocompleteEntity < Grape::Entity
+ expose :iid
+ expose :title
+ expose :reference do |issuable, options|
+ issuable.to_reference(options[:parent_group])
+ end
+end
diff --git a/app/serializers/group_issuable_autocomplete_serializer.rb b/app/serializers/group_issuable_autocomplete_serializer.rb
new file mode 100644
index 00000000000..59e9201d405
--- /dev/null
+++ b/app/serializers/group_issuable_autocomplete_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class GroupIssuableAutocompleteSerializer < BaseSerializer
+ entity GroupIssuableAutocompleteEntity
+end
diff --git a/app/services/ci/generate_codequality_mr_diff_report_service.rb b/app/services/ci/generate_codequality_mr_diff_report_service.rb
index 50f5f894ba4..117b0a21eaa 100644
--- a/app/services/ci/generate_codequality_mr_diff_report_service.rb
+++ b/app/services/ci/generate_codequality_mr_diff_report_service.rb
@@ -12,7 +12,7 @@ module Ci
{
status: :parsed,
key: key(base_pipeline, head_pipeline),
- data: head_pipeline.pipeline_artifacts.find_by_file_type(:code_quality_mr_diff).present.for_files(merge_request.new_paths)
+ data: head_pipeline.pipeline_artifacts.find_by_file_type(:code_quality_mr_diff).present.for_files(merge_request)
}
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
diff --git a/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb b/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb
index 5c52eef7ba6..d6865efac9f 100644
--- a/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb
+++ b/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb
@@ -2,11 +2,18 @@
module Ci
module PipelineArtifacts
class CreateCodeQualityMrDiffReportService
- def execute(pipeline)
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(pipeline)
+ @pipeline = pipeline
+ end
+
+ def execute
return unless pipeline.can_generate_codequality_reports?
return if pipeline.has_codequality_mr_diff_report?
+ return unless new_errors_introduced?
- file = build_carrierwave_file(pipeline)
+ file = build_carrierwave_file!
pipeline.pipeline_artifacts.create!(
project_id: pipeline.project_id,
@@ -20,18 +27,54 @@ module Ci
private
- def build_carrierwave_file(pipeline)
+ attr_reader :pipeline
+
+ def merge_requests
+ strong_memoize(:merge_requests) do
+ pipeline.merge_requests_as_head_pipeline
+ end
+ end
+
+ def head_report
+ strong_memoize(:head_report) do
+ pipeline.codequality_reports
+ end
+ end
+
+ def base_report(merge_request)
+ strong_memoize(:base_report) do
+ merge_request&.base_pipeline&.codequality_reports
+ end
+ end
+
+ def mr_diff_report_by_merge_requests
+ strong_memoize(:mr_diff_report_by_merge_requests) do
+ merge_requests.each_with_object({}) do |merge_request, hash|
+ key = "merge_request_#{merge_request.id}"
+ new_errors = Gitlab::Ci::Reports::CodequalityReportsComparer.new(base_report(merge_request), head_report).new_errors
+ next if new_errors.empty?
+
+ hash[key] = Gitlab::Ci::Reports::CodequalityMrDiff.new(new_errors)
+ end
+ end
+ end
+
+ def new_errors_introduced?
+ mr_diff_report_by_merge_requests.present?
+ end
+
+ def build_carrierwave_file!
CarrierWaveStringFile.new_file(
- file_content: build_quality_mr_diff_report(pipeline),
+ file_content: build_quality_mr_diff_report(mr_diff_report_by_merge_requests),
filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_quality_mr_diff),
content_type: 'application/json'
)
end
- def build_quality_mr_diff_report(pipeline)
- mr_diff_report = Gitlab::Ci::Reports::CodequalityMrDiff.new(pipeline.codequality_reports)
-
- Ci::CodequalityMrDiffReportSerializer.new.represent(mr_diff_report).to_json # rubocop: disable CodeReuse/Serializer
+ def build_quality_mr_diff_report(mr_diff_report)
+ mr_diff_report.each_with_object({}) do |diff_report, hash|
+ hash[diff_report.first] = Ci::CodequalityMrDiffReportSerializer.new.represent(diff_report.second) # rubocop: disable CodeReuse/Serializer
+ end.to_json
end
end
end
diff --git a/app/services/groups/autocomplete_service.rb b/app/services/groups/autocomplete_service.rb
new file mode 100644
index 00000000000..14f11bb3cfd
--- /dev/null
+++ b/app/services/groups/autocomplete_service.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Groups
+ class AutocompleteService < Groups::BaseService
+ include LabelsAsHash
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def issues(confidential_only: false, issue_types: nil)
+ finder_params = { group_id: group.id, include_subgroups: true, state: 'opened' }
+ finder_params[:confidential] = true if confidential_only.present?
+ finder_params[:issue_types] = issue_types if issue_types.present?
+
+ IssuesFinder.new(current_user, finder_params)
+ .execute
+ .preload(project: :namespace)
+ .select(:iid, :title, :project_id)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def merge_requests
+ MergeRequestsFinder.new(current_user, group_id: group.id, include_subgroups: true, state: 'opened')
+ .execute
+ .preload(target_project: :namespace)
+ .select(:iid, :title, :target_project_id)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def milestones
+ group_ids = group.self_and_ancestors.public_or_visible_to_user(current_user).pluck(:id)
+
+ MilestonesFinder.new(group_ids: group_ids).execute.select(:iid, :title, :due_date)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def labels_as_hash(target)
+ super(target, group_id: group.id, only_group_labels: true, include_ancestor_groups: true)
+ end
+
+ def commands(noteable)
+ return [] unless noteable
+
+ QuickActions::InterpretService.new(nil, current_user).available_commands(noteable)
+ end
+ end
+end
+
+Groups::AutocompleteService.prepend_if_ee('EE::Groups::AutocompleteService')
diff --git a/app/services/groups/participants_service.rb b/app/services/groups/participants_service.rb
new file mode 100644
index 00000000000..0844c98dd6a
--- /dev/null
+++ b/app/services/groups/participants_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Groups
+ class ParticipantsService < Groups::BaseService
+ include Users::ParticipableService
+
+ def execute(noteable)
+ @noteable = noteable
+
+ participants =
+ noteable_owner +
+ participants_in_noteable +
+ all_members +
+ groups +
+ group_members
+
+ render_participants_as_hash(participants.uniq)
+ end
+
+ def all_members
+ count = group_members.count
+ [{ username: "all", name: "All Group Members", count: count }]
+ end
+
+ def group_members
+ return [] unless noteable
+
+ @group_members ||= sorted(noteable.group.direct_and_indirect_users)
+ end
+ end
+end
diff --git a/app/views/admin/dev_ops_report/_no_data.html.haml b/app/views/admin/dev_ops_report/_no_data.html.haml
deleted file mode 100644
index e540a4e2bce..00000000000
--- a/app/views/admin/dev_ops_report/_no_data.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-.container.devops-empty
- .col-sm-12.justify-content-center.text-center
- = custom_icon('dev_ops_report_no_data')
- %h4= _('Data is still calculating...')
- %p
- = _('It may be several days before you see feature usage data.')
- = link_to _('Our documentation includes an example DevOps Score report.'), help_page_path('user/admin_area/analytics/dev_ops_report'), target: '_blank'
diff --git a/app/views/admin/dev_ops_report/_report.html.haml b/app/views/admin/dev_ops_report/_report.html.haml
index db68a7a2291..dbd0020e382 100644
--- a/app/views/admin/dev_ops_report/_report.html.haml
+++ b/app/views/admin/dev_ops_report/_report.html.haml
@@ -4,9 +4,7 @@
= render 'callout'
- if !usage_ping_enabled
- #js-devops-empty-state{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/usage_ping/index.md') } }
-- elsif @metric.blank?
- = render 'no_data'
+ #js-devops-usage-ping-disabled{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/usage_ping/index.md') } }
- else
- #js-devops-score{ data: { devops_score_metrics: devops_score_metrics(@metric).to_json } }
+ #js-devops-score{ data: { devops_score_metrics: devops_score_metrics(@metric).to_json, devops_report_docs_path: help_page_path('user/admin_area/analytics/dev_ops_report'), no_data_image_path: image_path('dev_ops_report_no_data.svg') } }
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index d4d8a7a57ef..259e96901fd 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -12,7 +12,11 @@
= f.label :description, _("Description")
.col-sm-10
= render layout: 'shared/md_preview', locals: { url: group_preview_markdown_path } do
- = render 'shared/zen', f: f, attr: :description, classes: 'note-textarea', qa_selector: 'milestone_description_field', placeholder: _('Write milestone description...'), supports_autocomplete: false
+ = render 'shared/zen', f: f, attr: :description,
+ classes: 'note-textarea',
+ qa_selector: 'milestone_description_field',
+ supports_autocomplete: true,
+ placeholder: _('Write milestone description...')
.clearfix
.error-alert
= render "shared/milestones/form_dates", f: f
diff --git a/app/views/groups/settings/packages_and_registries/index.html.haml b/app/views/groups/settings/packages_and_registries/show.html.haml
similarity index 100%
rename from app/views/groups/settings/packages_and_registries/index.html.haml
rename to app/views/groups/settings/packages_and_registries/show.html.haml
diff --git a/app/views/layouts/nav/sidebar/_project_menus.html.haml b/app/views/layouts/nav/sidebar/_project_menus.html.haml
index ea04d14dcda..951a8bf437d 100644
--- a/app/views/layouts/nav/sidebar/_project_menus.html.haml
+++ b/app/views/layouts/nav/sidebar/_project_menus.html.haml
@@ -1,34 +1,3 @@
-- if project_nav_tab?(:confluence)
- - confluence_url = project_wikis_confluence_path(@project)
- = nav_link do
- = link_to confluence_url, class: 'shortcuts-confluence' do
- .nav-icon-container
- = image_tag 'confluence.svg', alt: _('Confluence')
- %span.nav-item-name
- = _('Confluence')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(html_options: { class: 'fly-out-top-item' } ) do
- = link_to confluence_url, target: '_blank', rel: 'noopener noreferrer' do
- %strong.fly-out-top-item-name
- = _('Confluence')
-
-- if project_nav_tab? :wiki
- = render 'layouts/nav/sidebar/wiki_link', wiki_url: wiki_path(@project.wiki)
-
-- if project_nav_tab?(:external_wiki)
- - external_wiki_url = @project.external_wiki.external_wiki_url
- = nav_link do
- = link_to external_wiki_url, class: 'shortcuts-external_wiki' do
- .nav-icon-container
- = sprite_icon('external-link')
- %span.nav-item-name
- = s_('ExternalWikiService|External wiki')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(html_options: { class: "fly-out-top-item" } ) do
- = link_to external_wiki_url do
- %strong.fly-out-top-item-name
- = s_('ExternalWikiService|External wiki')
-
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index a6adfd31834..dfb9defb91c 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -13,7 +13,11 @@
= f.label :description, _('Description')
.col-sm-10
= render layout: 'shared/md_preview', locals: { url: preview_markdown_path(@project) } do
- = render 'shared/zen', f: f, attr: :description, classes: 'note-textarea', qa_selector: 'milestone_description_field', placeholder: _('Write milestone description...')
+ = render 'shared/zen', f: f, attr: :description,
+ classes: 'note-textarea',
+ qa_selector: 'milestone_description_field',
+ supports_autocomplete: true,
+ placeholder: _('Write milestone description...')
= render 'shared/notes/hints'
.clearfix
.error-alert
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 628d36b86d9..e06e278f2e1 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -4,7 +4,7 @@
.js-remove-member-modal
.row.gl-mt-3
.col-lg-12
- - if can_invite_members_for_project?(@project)
+ - if can_invite_members_for_project?(@project) || can_invite_group_for_project?(@project)
.row
.col-md-12.col-lg-6.gl-display-flex
.gl-flex-direction-column.gl-flex-wrap.align-items-baseline
@@ -18,12 +18,15 @@
= html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: ''.html_safe, i_close: ''.html_safe }
.col-md-12.col-lg-6
.gl-display-flex.gl-flex-wrap.gl-justify-content-end
- = link_to _("Import a project"),
- import_project_project_members_path(@project),
- class: "btn btn-default btn-md gl-button gl-mt-3 gl-sm-w-auto gl-w-full",
- title: _("Import members from another project")
- .js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite a group') } }
- .js-invite-members-trigger{ data: { variant: 'success', classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
+ - if can_import_members?
+ = link_to _("Import a project"),
+ import_project_project_members_path(@project),
+ class: "btn btn-default btn-md gl-button gl-mt-3 gl-sm-w-auto gl-w-full",
+ title: _("Import members from another project")
+ - if @project.allowed_to_share_with_group?
+ .js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite a group') } }
+ - if !membership_locked?
+ .js-invite-members-trigger{ data: { variant: 'success', classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
= render 'projects/invite_members_modal', project: @project
- else
@@ -36,7 +39,7 @@
%p
= html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: ''.html_safe, i_close: ''.html_safe }
- - if !can_invite_members_for_project?(@project) && can_manage_project_members?(@project) && project_can_be_shared?
+ - if Feature.disabled?(:invite_members_group_modal, @project.group) && can_manage_project_members?(@project) && project_can_be_shared?
- if !membership_locked? && @project.allowed_to_share_with_group?
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
%li.nav-tab{ role: 'presentation' }
diff --git a/app/views/projects/settings/packages_and_registries/index.html.haml b/app/views/projects/settings/packages_and_registries/show.html.haml
similarity index 100%
rename from app/views/projects/settings/packages_and_registries/index.html.haml
rename to app/views/projects/settings/packages_and_registries/show.html.haml
diff --git a/app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb b/app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb
index d83c7f6571e..558153c69b2 100644
--- a/app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb
+++ b/app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb
@@ -15,7 +15,7 @@ module Ci
def perform(pipeline_id)
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
- Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService.new.execute(pipeline)
+ Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService.new(pipeline).execute
end
end
end
diff --git a/changelogs/unreleased/271244-fe-devops-report-convert-score-page-to-vue-components-2.yml b/changelogs/unreleased/271244-fe-devops-report-convert-score-page-to-vue-components-2.yml
new file mode 100644
index 00000000000..c7b4f095fe5
--- /dev/null
+++ b/changelogs/unreleased/271244-fe-devops-report-convert-score-page-to-vue-components-2.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate DevOps Score empty state to Vue
+merge_request: 60715
+author:
+type: changed
diff --git a/changelogs/unreleased/322017-updating-hover-focus-states-on-pipeline-buttons.yml b/changelogs/unreleased/322017-updating-hover-focus-states-on-pipeline-buttons.yml
new file mode 100644
index 00000000000..231ad27a07f
--- /dev/null
+++ b/changelogs/unreleased/322017-updating-hover-focus-states-on-pipeline-buttons.yml
@@ -0,0 +1,5 @@
+---
+title: updating hover state to match other pipeline graph buttons
+merge_request: 60801
+author: Matt Saddington @mattsaddo
+type: other
diff --git a/changelogs/unreleased/329695-add-read_commit_status-field-authorization-to-pipeline-fields-that.yml b/changelogs/unreleased/329695-add-read_commit_status-field-authorization-to-pipeline-fields-that.yml
new file mode 100644
index 00000000000..16ebd6eb8a7
--- /dev/null
+++ b/changelogs/unreleased/329695-add-read_commit_status-field-authorization-to-pipeline-fields-that.yml
@@ -0,0 +1,5 @@
+---
+title: Adds field authorization to pipeline fields
+merge_request: 60754
+author:
+type: changed
diff --git a/changelogs/unreleased/330086-inverse-index-on-namespaces-parent_id-id.yml b/changelogs/unreleased/330086-inverse-index-on-namespaces-parent_id-id.yml
new file mode 100644
index 00000000000..6e320025114
--- /dev/null
+++ b/changelogs/unreleased/330086-inverse-index-on-namespaces-parent_id-id.yml
@@ -0,0 +1,5 @@
+---
+title: Partial index optimization for namespaces id
+merge_request: 61098
+author:
+type: performance
diff --git a/changelogs/unreleased/expose-mr-timelogs-via-graphql.yml b/changelogs/unreleased/expose-mr-timelogs-via-graphql.yml
new file mode 100644
index 00000000000..2f0e683d972
--- /dev/null
+++ b/changelogs/unreleased/expose-mr-timelogs-via-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Expose merge request timelogs via GraphQL
+merge_request: 57322
+author: Lee Tickett @leetickett
+type: added
diff --git a/changelogs/unreleased/feat-milestone-description-auto-complete.yml b/changelogs/unreleased/feat-milestone-description-auto-complete.yml
new file mode 100644
index 00000000000..dcde7361282
--- /dev/null
+++ b/changelogs/unreleased/feat-milestone-description-auto-complete.yml
@@ -0,0 +1,5 @@
+---
+title: Add autocomplete to milestone description
+merge_request: 59564
+author: Jonas Wälter @wwwjon
+type: added
diff --git a/changelogs/unreleased/fix-openshift-template-to-run-on-main-branch.yml b/changelogs/unreleased/fix-openshift-template-to-run-on-main-branch.yml
new file mode 100644
index 00000000000..c11633592db
--- /dev/null
+++ b/changelogs/unreleased/fix-openshift-template-to-run-on-main-branch.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Openshift template to run on main branch
+merge_request: 60811
+author:
+type: fixed
diff --git a/changelogs/unreleased/mo-fix-codequality-mr-diff-report.yml b/changelogs/unreleased/mo-fix-codequality-mr-diff-report.yml
new file mode 100644
index 00000000000..cafdd8290db
--- /dev/null
+++ b/changelogs/unreleased/mo-fix-codequality-mr-diff-report.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fix false positive for codequality mr diff report'
+merge_request: 59421
+author:
+type: fixed
diff --git a/changelogs/unreleased/remove-usage_data_track_ecosystem_slack_service-feature-flag.yml b/changelogs/unreleased/remove-usage_data_track_ecosystem_slack_service-feature-flag.yml
new file mode 100644
index 00000000000..346103464f3
--- /dev/null
+++ b/changelogs/unreleased/remove-usage_data_track_ecosystem_slack_service-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Add slack integration individual usage ping
+merge_request: 60847
+author:
+type: changed
diff --git a/config/feature_flags/development/usage_data_track_ecosystem_slack_service.yml b/config/feature_flags/development/cached_encoding_detection.yml
similarity index 60%
rename from config/feature_flags/development/usage_data_track_ecosystem_slack_service.yml
rename to config/feature_flags/development/cached_encoding_detection.yml
index d9d4c06c9fa..362c465cfb6 100644
--- a/config/feature_flags/development/usage_data_track_ecosystem_slack_service.yml
+++ b/config/feature_flags/development/cached_encoding_detection.yml
@@ -1,8 +1,8 @@
---
-name: usage_data_track_ecosystem_slack_service
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54347
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/322588
-milestone: '13.10'
+name: cached_encoding_detection
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60128
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328819
+milestone: '13.12'
type: development
-group: group::ecosystem
+group: group::source code
default_enabled: false
diff --git a/config/feature_flags/development/find_remote_root_refs_inmemory.yml b/config/feature_flags/development/find_remote_root_refs_inmemory.yml
new file mode 100644
index 00000000000..18e2e2b366a
--- /dev/null
+++ b/config/feature_flags/development/find_remote_root_refs_inmemory.yml
@@ -0,0 +1,8 @@
+---
+name: find_remote_root_refs_inmemory
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60583
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329664
+milestone: '13.12'
+type: development
+group: group::gitaly
+default_enabled: true
diff --git a/config/feature_flags/development/limited_diff_highlighting.yml b/config/feature_flags/development/limited_diff_highlighting.yml
new file mode 100644
index 00000000000..1cfd9927c7a
--- /dev/null
+++ b/config/feature_flags/development/limited_diff_highlighting.yml
@@ -0,0 +1,8 @@
+---
+name: limited_diff_highlighting
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53768
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323566
+milestone: '13.12'
+type: development
+group: group::source code
+default_enabled: true
diff --git a/config/feature_flags/development/ci_no_empty_groups.yml b/config/feature_flags/development/track_highlight_timeouts.yml
similarity index 63%
rename from config/feature_flags/development/ci_no_empty_groups.yml
rename to config/feature_flags/development/track_highlight_timeouts.yml
index ef7d6459a5a..a85749e5187 100644
--- a/config/feature_flags/development/ci_no_empty_groups.yml
+++ b/config/feature_flags/development/track_highlight_timeouts.yml
@@ -1,8 +1,8 @@
---
-name: ci_no_empty_groups
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58789
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327139
-milestone: '13.11'
+name: track_highlight_timeouts
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60956
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329909
+milestone: '13.12'
type: development
-group: group::verify
+group: group::code review
default_enabled: false
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 126680a0b44..ef31b639d33 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -52,7 +52,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resources :applications
- resources :packages_and_registries, only: [:index]
+ resource :packages_and_registries, only: [:show]
end
resource :variables, only: [:show, :update]
@@ -114,6 +114,17 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resources :container_registries, only: [:index, :show], controller: 'registry/repositories'
resource :dependency_proxy, only: [:show, :update]
resources :email_campaigns, only: :index
+
+ resources :autocomplete_sources, only: [] do
+ collection do
+ get 'members'
+ get 'issues'
+ get 'merge_requests'
+ get 'labels'
+ get 'commands'
+ get 'milestones'
+ end
+ end
end
scope(path: '*id',
diff --git a/config/routes/project.rb b/config/routes/project.rb
index edce34acc67..43b0e2a5568 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -130,7 +130,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :packages_and_registries, only: [:index]
+ resource :packages_and_registries, only: [:show]
end
resources :autocomplete_sources, only: [] do
diff --git a/db/post_migrate/20210506064413_create_namespaces_id_parent_id_inverse_partial_index.rb b/db/post_migrate/20210506064413_create_namespaces_id_parent_id_inverse_partial_index.rb
new file mode 100644
index 00000000000..151b0f64e80
--- /dev/null
+++ b/db/post_migrate/20210506064413_create_namespaces_id_parent_id_inverse_partial_index.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class CreateNamespacesIdParentIdInversePartialIndex < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ NAME = 'index_namespaces_id_parent_id_is_not_null'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :namespaces, :id, where: 'parent_id IS NOT NULL', name: NAME
+ end
+
+ def down
+ remove_concurrent_index :namespaces, :id, name: NAME
+ end
+end
diff --git a/db/schema_migrations/20210506064413 b/db/schema_migrations/20210506064413
new file mode 100644
index 00000000000..72e3336d898
--- /dev/null
+++ b/db/schema_migrations/20210506064413
@@ -0,0 +1 @@
+f400225e6caa854f825422b9799e61ea557ab4bd3e4a33dc3cd3193ed3ce1db2
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 5837926893e..c962794e928 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -23454,6 +23454,8 @@ CREATE UNIQUE INDEX index_namespace_root_storage_statistics_on_namespace_id ON n
CREATE UNIQUE INDEX index_namespace_statistics_on_namespace_id ON namespace_statistics USING btree (namespace_id);
+CREATE INDEX index_namespaces_id_parent_id_is_not_null ON namespaces USING btree (id) WHERE (parent_id IS NOT NULL);
+
CREATE INDEX index_namespaces_id_parent_id_is_null ON namespaces USING btree (id) WHERE (parent_id IS NULL);
CREATE INDEX index_namespaces_on_created_at ON namespaces USING btree (created_at);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index b55a2000d69..7f9bd217224 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -9005,7 +9005,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
##### `Group.timelogs`
-Time logged on issues in the group and its subgroups.
+Time logged on issues and merge requests in the group and its subgroups.
Returns [`TimelogConnection!`](#timelogconnection).
@@ -12498,6 +12498,7 @@ Represents a historically accurate report about the timebox.
| Name | Type | Description |
| ---- | ---- | ----------- |
| `issue` | [`Issue`](#issue) | The issue that logged time was added to. |
+| `mergeRequest` | [`MergeRequest`](#mergerequest) | The merge request that logged time was added to. |
| `note` | [`Note`](#note) | The note where the quick action to add the logged time was executed. |
| `spentAt` | [`Time`](#time) | Timestamp of when the time tracked was spent at. |
| `timeSpent` | [`Int!`](#int) | The time spent displayed in seconds. |
diff --git a/doc/user/group/devops_adoption/index.md b/doc/user/group/devops_adoption/index.md
index 920421ef9bb..a83c4fcc6f6 100644
--- a/doc/user/group/devops_adoption/index.md
+++ b/doc/user/group/devops_adoption/index.md
@@ -6,33 +6,36 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Group DevOps Adoption **(ULTIMATE)**
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321083) in GitLab 13.11.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321083) as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta) in GitLab 13.11.
> - [Deployed behind a feature flag](../../../user/feature_flags.md), disabled by default.
> - Disabled on GitLab.com.
> - Not recommended for production use.
> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-group-devops-adoption).
-WARNING:
-This feature might not be available to you. Check the **version history** note above for details.
+This in-development feature might not be available for your use. There can be
+[risks when enabling features still in development](../../feature_flags.md#risks-when-enabling-features-still-in-development).
+Refer to this feature's version history for more details.
-[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321083) in GitLab 13.11 as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta).
+Prerequisites:
-To access Group DevOps Adoption, navigate to your group sidebar and select **Analytics > DevOps Adoption**
+- A minimum of [Reporter access](../../permissions.md) to the group.
+
+To access Group DevOps Adoption, go to your group and select **Analytics > DevOps Adoption**.
Group DevOps Adoption shows you how individual groups and sub-groups within your organization use the following features:
+- Approvals
+- Deployments
- Issues
- Merge Requests
-- Approvals
-- Runners
- Pipelines
-- Deployments
+- Runners
- Scans
When managing groups in the UI, you can manage your sub-groups with the **Add/Remove sub-groups**
button, in the top right hand section of your Groups pages.
-DevOps Adoption allows you to:
+With DevOps Adoption you can:
- Verify whether you are getting the return on investment that you expected from GitLab.
- Identify specific sub-groups that are lagging in their adoption of GitLab so you can help them along in their DevOps journey.
diff --git a/doc/user/project/clusters/add_eks_clusters.md b/doc/user/project/clusters/add_eks_clusters.md
index e329ec4f903..c0fb8f5848f 100644
--- a/doc/user/project/clusters/add_eks_clusters.md
+++ b/doc/user/project/clusters/add_eks_clusters.md
@@ -41,9 +41,9 @@ For example, the following policy document allows assuming a role whose name sta
}
```
-### Administration settings
+### Configure Amazon authentication
-Generate an access key for the IAM user, and configure GitLab with the credentials:
+To configure Amazon authentication in GitLab, generate an access key for the IAM user in the Amazon AWS console, and following the steps below.
1. Navigate to **Admin Area > Settings > General** and expand the **Amazon EKS** section.
1. Check **Enable Amazon EKS integration**.
@@ -232,7 +232,7 @@ sequenceDiagram
First, GitLab must obtain an initial set of credentials to communicate with the AWS API.
These credentials can be retrieved in one of two ways:
-- Statically through the [Administration settings](#administration-settings).
+- Statically through the [Configure Amazon authentication](#configure-amazon-authentication).
- Dynamically via an IAM instance profile ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291015) in GitLab 13.7).
After GitLab retrieves the AWS credentials, it makes an
@@ -272,7 +272,7 @@ arn:aws:iam::123456789012:role/gitlab-eks-provision'
#### Access denied: User `arn:aws:iam::x` is not authorized to perform: `sts:AssumeRole` on resource: `arn:aws:iam::y`
This error occurs when the credentials defined in the
-[Administration settings](#administration-settings) cannot assume the role defined by the
+[Configure Amazon authentication](#configure-amazon-authentication) cannot assume the role defined by the
Provision Role ARN. Check that:
1. The initial set of AWS credentials [has the AssumeRole policy](#additional-requirements-for-self-managed-instances).
@@ -290,6 +290,10 @@ because GitLab has successfully assumed your provided role, but the role has
insufficient permissions to retrieve the resources needed for the form. Make sure
you've assigned the role the correct permissions.
+### Key Pairs are not loaded
+
+GitLab loads the key pairs from the **Cluster Region** specified. Ensure that key pair exists in that region.
+
#### `ROLLBACK_FAILED` during cluster creation
The creation process halted because GitLab encountered an error when creating
diff --git a/doc/user/project/time_tracking.md b/doc/user/project/time_tracking.md
index 0bde6045329..44bffe763e6 100644
--- a/doc/user/project/time_tracking.md
+++ b/doc/user/project/time_tracking.md
@@ -99,3 +99,8 @@ With this option enabled, `75h` is displayed instead of `1w 4d 3h`.
## Other interesting links
- [Time Tracking solutions page](https://about.gitlab.com/solutions/time-tracking/)
+- Time Tracking GraphQL references:
+ - [Connection](../../api/graphql/reference/index.md#timelogconnection)
+ - [Edge](../../api/graphql/reference/index.md#timelogedge)
+ - [Fields](../../api/graphql/reference/index.md#timelog)
+ - [Group Timelogs](../../api/graphql/reference/index.md#grouptimelogs)
diff --git a/lib/gitlab/blob_helper.rb b/lib/gitlab/blob_helper.rb
index 57d632afd74..c5b183d113d 100644
--- a/lib/gitlab/blob_helper.rb
+++ b/lib/gitlab/blob_helper.rb
@@ -38,7 +38,7 @@ module Gitlab
# If Charlock says its binary
else
- detect_encoding[:type] == :binary
+ find_encoding[:type] == :binary
end
end
@@ -137,23 +137,25 @@ module Gitlab
end
def ruby_encoding
- if hash = detect_encoding
+ if hash = find_encoding
hash[:ruby_encoding]
end
end
def encoding
- if hash = detect_encoding
+ if hash = find_encoding
hash[:encoding]
end
end
- def detect_encoding
- @detect_encoding ||= CharlockHolmes::EncodingDetector.new.detect(data) if data # rubocop:disable Gitlab/ModuleWithInstanceVariables
- end
-
def empty?
data.nil? || data == ""
end
+
+ private
+
+ def find_encoding
+ @find_encoding ||= Gitlab::EncodingHelper.detect_encoding(data) if data # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
end
end
diff --git a/lib/gitlab/ci/reports/codequality_mr_diff.rb b/lib/gitlab/ci/reports/codequality_mr_diff.rb
index e60a075e3f5..0595b6f966a 100644
--- a/lib/gitlab/ci/reports/codequality_mr_diff.rb
+++ b/lib/gitlab/ci/reports/codequality_mr_diff.rb
@@ -6,8 +6,8 @@ module Gitlab
class CodequalityMrDiff
attr_reader :files
- def initialize(raw_report)
- @raw_report = raw_report
+ def initialize(new_errors)
+ @new_errors = new_errors
@files = {}
build_report!
end
@@ -15,7 +15,7 @@ module Gitlab
private
def build_report!
- codequality_files = @raw_report.all_degradations.each_with_object({}) do |degradation, codequality_files|
+ codequality_files = @new_errors.each_with_object({}) do |degradation, codequality_files|
unless codequality_files[degradation.dig(:location, :path)].present?
codequality_files[degradation.dig(:location, :path)] = []
end
diff --git a/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml b/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml
index 3faf07546de..45bddb1bc6a 100644
--- a/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml
@@ -46,27 +46,23 @@ review:
name: review/$CI_COMMIT_REF_NAME
url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$OPENSHIFT_DOMAIN
on_stop: stop-review
- only:
- - branches
- except:
- - master
+ rules:
+ - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
stop-review:
<<: *deploy
stage: cleanup
script:
- oc delete all -l "app=$APP"
- when: manual
variables:
APP: review-$CI_COMMIT_REF_NAME
GIT_STRATEGY: none
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
- only:
- - branches
- except:
- - master
+ rules:
+ - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
+ when: manual
staging:
<<: *deploy
@@ -77,8 +73,8 @@ staging:
environment:
name: staging
url: http://$CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN
- only:
- - master
+ rules:
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
production:
<<: *deploy
@@ -86,9 +82,9 @@ production:
variables:
APP: production
APP_HOST: $CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
- when: manual
environment:
name: production
url: http://$CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
- only:
- - master
+ rules:
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+ when: manual
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index d86eb83083b..6a41ed0f29e 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -87,6 +87,7 @@ module Gitlab
def highlight_line(diff_line)
return unless diff_file && diff_file.diff_refs
+ return diff_line_highlighting(diff_line, plain: true) if blobs_too_large?
if Feature.enabled?(:diff_line_syntax_highlighting, project, default_enabled: :yaml)
diff_line_highlighting(diff_line)
@@ -95,9 +96,10 @@ module Gitlab
end
end
- def diff_line_highlighting(diff_line)
+ def diff_line_highlighting(diff_line, plain: false)
rich_line = syntax_highlighter(diff_line).highlight(
diff_line.text(prefix: false),
+ plain: plain,
context: { line_number: diff_line.line }
)
@@ -158,6 +160,13 @@ module Gitlab
blob.load_all_data!
blob.present.highlight.lines
end
+
+ def blobs_too_large?
+ return false unless Feature.enabled?(:limited_diff_highlighting, project, default_enabled: :yaml)
+ return true if Gitlab::Highlight.too_large?(diff_file.old_blob&.size)
+
+ Gitlab::Highlight.too_large?(diff_file.new_blob&.size)
+ end
end
end
end
diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb
index 7b79de00c66..8ee53d0de28 100644
--- a/lib/gitlab/encoding_helper.rb
+++ b/lib/gitlab/encoding_helper.rb
@@ -20,7 +20,7 @@ module Gitlab
return message if message.valid_encoding?
# return message if message type is binary
- detect = CharlockHolmes::EncodingDetector.detect(message)
+ detect = detect_encoding(message)
return message.force_encoding("BINARY") if detect_binary?(message, detect)
if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
@@ -37,16 +37,30 @@ module Gitlab
"--broken encoding: #{encoding}"
end
+ def detect_encoding(data, limit: CharlockHolmes::EncodingDetector::DEFAULT_BINARY_SCAN_LEN, cache_key: nil)
+ return if data.nil?
+
+ if Feature.enabled?(:cached_encoding_detection, type: :development, default_enabled: :yaml)
+ return CharlockHolmes::EncodingDetector.new(limit).detect(data) unless cache_key.present?
+
+ Rails.cache.fetch([:detect_binary, CharlockHolmes::VERSION, cache_key], expires_in: 1.week) do
+ CharlockHolmes::EncodingDetector.new(limit).detect(data)
+ end
+ else
+ CharlockHolmes::EncodingDetector.new(limit).detect(data)
+ end
+ end
+
def detect_binary?(data, detect = nil)
- detect ||= CharlockHolmes::EncodingDetector.detect(data)
+ detect ||= detect_encoding(data)
detect && detect[:type] == :binary && detect[:confidence] == 100
end
- def detect_libgit2_binary?(data)
- # EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks
- # only the first 8000 (https://github.com/libgit2/libgit2/blob/2ed855a9e8f9af211e7274021c2264e600c0f86b/src/filter.h#L15),
- # which is what we use below to keep a consistent behavior.
- detect = CharlockHolmes::EncodingDetector.new(8000).detect(data)
+ # EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks
+ # only the first 8000 (https://github.com/libgit2/libgit2/blob/2ed855a9e8f9af211e7274021c2264e600c0f86b/src/filter.h#L15),
+ # which is what we use below to keep a consistent behavior.
+ def detect_libgit2_binary?(data, cache_key: nil)
+ detect = detect_encoding(data, limit: 8000, cache_key: cache_key)
detect && detect[:type] == :binary
end
@@ -54,7 +68,8 @@ module Gitlab
message = force_encode_utf8(message)
return message if message.valid_encoding?
- detect = CharlockHolmes::EncodingDetector.detect(message)
+ detect = detect_encoding(message)
+
if detect && detect[:encoding]
begin
CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 5d91eb605e8..1c8e55ecf50 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -110,8 +110,8 @@ module Gitlab
end
end
- def binary?(data)
- EncodingHelper.detect_libgit2_binary?(data)
+ def binary?(data, cache_key: nil)
+ EncodingHelper.detect_libgit2_binary?(data, cache_key: cache_key)
end
def size_could_be_lfs?(size)
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 45cf8f5572a..102fe60f2cb 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -700,11 +700,11 @@ module Gitlab
end
end
- def find_remote_root_ref(remote_name)
- return unless remote_name.present?
+ def find_remote_root_ref(remote_name, remote_url, authorization = nil)
+ return unless remote_name.present? && remote_url.present?
wrapped_gitaly_errors do
- gitaly_remote_client.find_remote_root_ref(remote_name)
+ gitaly_remote_client.find_remote_root_ref(remote_name, remote_url, authorization)
end
end
diff --git a/lib/gitlab/gitaly_client/blobs_stitcher.rb b/lib/gitlab/gitaly_client/blobs_stitcher.rb
index a0badb2f406..2f6d146b5c4 100644
--- a/lib/gitlab/gitaly_client/blobs_stitcher.rb
+++ b/lib/gitlab/gitaly_client/blobs_stitcher.rb
@@ -41,7 +41,7 @@ module Gitlab
size: blob_data[:size],
commit_id: blob_data[:revision],
data: data,
- binary: Gitlab::Git::Blob.binary?(data)
+ binary: Gitlab::Git::Blob.binary?(data, cache_key: blob_data[:oid])
)
end
end
diff --git a/lib/gitlab/gitaly_client/remote_service.rb b/lib/gitlab/gitaly_client/remote_service.rb
index 06aaf460751..04dd394a2bd 100644
--- a/lib/gitlab/gitaly_client/remote_service.rb
+++ b/lib/gitlab/gitaly_client/remote_service.rb
@@ -43,11 +43,20 @@ module Gitlab
GitalyClient.call(@storage, :remote_service, :remove_remote, request, timeout: GitalyClient.long_timeout).result
end
- def find_remote_root_ref(remote_name)
- request = Gitaly::FindRemoteRootRefRequest.new(
- repository: @gitaly_repo,
- remote: remote_name
- )
+ # The remote_name parameter is deprecated and will be removed soon.
+ def find_remote_root_ref(remote_name, remote_url, authorization)
+ request = if Feature.enabled?(:find_remote_root_refs_inmemory, default_enabled: :yaml)
+ Gitaly::FindRemoteRootRefRequest.new(
+ repository: @gitaly_repo,
+ remote_url: remote_url,
+ http_authorization_header: authorization
+ )
+ else
+ Gitaly::FindRemoteRootRefRequest.new(
+ repository: @gitaly_repo,
+ remote: remote_name
+ )
+ end
response = GitalyClient.call(@storage, :remote_service,
:find_remote_root_ref, request, timeout: GitalyClient.medium_timeout)
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 06558e1a212..45f17d73f30 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -10,6 +10,10 @@ module Gitlab
.highlight(blob_content, continue: false, plain: plain)
end
+ def self.too_large?(size)
+ size.to_i > Gitlab.config.extra['maximum_text_highlight_size_kilobytes']
+ end
+
attr_reader :blob_name
def initialize(blob_name, blob_content, language: nil)
@@ -22,7 +26,7 @@ module Gitlab
def highlight(text, continue: false, plain: false, context: {})
@context = context
- plain ||= text.length > maximum_text_highlight_size
+ plain ||= self.class.too_large?(text.length)
highlighted_text = highlight_text(text, continue: continue, plain: plain)
highlighted_text = link_dependencies(text, highlighted_text) if blob_name
@@ -64,6 +68,8 @@ module Gitlab
tokens = lexer.lex(text, continue: continue)
Timeout.timeout(timeout_time) { @formatter.format(tokens, context.merge(tag: tag)).html_safe }
rescue Timeout::Error => e
+ add_highlight_timeout_metric
+
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
highlight_plain(text)
rescue StandardError
@@ -78,8 +84,17 @@ module Gitlab
Gitlab::DependencyLinker.link(blob_name, text, highlighted_text)
end
- def maximum_text_highlight_size
- Gitlab.config.extra['maximum_text_highlight_size_kilobytes']
+ def add_highlight_timeout_metric
+ return unless Feature.enabled?(:track_highlight_timeouts)
+
+ highlight_timeout.increment(source: Gitlab::Runtime.sidekiq? ? "background" : "foreground")
+ end
+
+ def highlight_timeout
+ @highlight_timeout ||= Gitlab::Metrics.counter(
+ :highlight_timeout,
+ 'Counts the times highlights have timed out'
+ )
end
end
end
diff --git a/lib/gitlab/usage_data_counters/known_events/ecosystem.yml b/lib/gitlab/usage_data_counters/known_events/ecosystem.yml
index 1c765bb1830..adc5ba36ad7 100644
--- a/lib/gitlab/usage_data_counters/known_events/ecosystem.yml
+++ b/lib/gitlab/usage_data_counters/known_events/ecosystem.yml
@@ -24,45 +24,35 @@
category: ecosystem
redis_slot: ecosystem
aggregation: weekly
- feature_flag: usage_data_track_ecosystem_slack_service
- name: i_ecosystem_slack_service_push_notification
category: ecosystem
redis_slot: ecosystem
aggregation: weekly
- feature_flag: usage_data_track_ecosystem_slack_service
- name: i_ecosystem_slack_service_deployment_notification
category: ecosystem
redis_slot: ecosystem
aggregation: weekly
- feature_flag: usage_data_track_ecosystem_slack_service
- name: i_ecosystem_slack_service_wiki_page_notification
category: ecosystem
redis_slot: ecosystem
aggregation: weekly
- feature_flag: usage_data_track_ecosystem_slack_service
- name: i_ecosystem_slack_service_merge_request_notification
category: ecosystem
redis_slot: ecosystem
aggregation: weekly
- feature_flag: usage_data_track_ecosystem_slack_service
- name: i_ecosystem_slack_service_note_notification
category: ecosystem
redis_slot: ecosystem
aggregation: weekly
- feature_flag: usage_data_track_ecosystem_slack_service
- name: i_ecosystem_slack_service_tag_push_notification
category: ecosystem
redis_slot: ecosystem
aggregation: weekly
- feature_flag: usage_data_track_ecosystem_slack_service
- name: i_ecosystem_slack_service_confidential_note_notification
category: ecosystem
redis_slot: ecosystem
aggregation: weekly
- feature_flag: usage_data_track_ecosystem_slack_service
- name: i_ecosystem_slack_service_confidential_issue_notification
category: ecosystem
redis_slot: ecosystem
aggregation: weekly
- feature_flag: usage_data_track_ecosystem_slack_service
-
diff --git a/lib/sidebars/projects/menus/confluence_menu.rb b/lib/sidebars/projects/menus/confluence_menu.rb
new file mode 100644
index 00000000000..0d83238fa82
--- /dev/null
+++ b/lib/sidebars/projects/menus/confluence_menu.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ class ConfluenceMenu < ::Sidebars::Menu
+ override :link
+ def link
+ project_wikis_confluence_path(context.project)
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ class: 'shortcuts-confluence'
+ }
+ end
+
+ override :title
+ def title
+ _('Confluence')
+ end
+
+ override :image_path
+ def image_path
+ 'confluence.svg'
+ end
+
+ override :image_html_options
+ def image_html_options
+ {
+ alt: title
+ }
+ end
+
+ override :render?
+ def render?
+ context.project.has_confluence?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/projects/menus/external_wiki_menu.rb b/lib/sidebars/projects/menus/external_wiki_menu.rb
new file mode 100644
index 00000000000..825f0ca5e8b
--- /dev/null
+++ b/lib/sidebars/projects/menus/external_wiki_menu.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ class ExternalWikiMenu < ::Sidebars::Menu
+ override :link
+ def link
+ external_wiki.external_wiki_url
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ target: '_blank',
+ rel: 'noopener noreferrer',
+ class: 'shortcuts-external_wiki'
+ }
+ end
+
+ override :extra_collapsed_container_html_options
+ def extra_collapsed_container_html_options
+ {
+ target: '_blank',
+ rel: 'noopener noreferrer'
+ }
+ end
+
+ override :title
+ def title
+ s_('ExternalWikiService|External wiki')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'external-link'
+ end
+
+ override :render?
+ def render?
+ external_wiki.present?
+ end
+
+ private
+
+ def external_wiki
+ @external_wiki ||= context.project.external_wiki
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/projects/menus/wiki_menu.rb b/lib/sidebars/projects/menus/wiki_menu.rb
new file mode 100644
index 00000000000..3980b193fd1
--- /dev/null
+++ b/lib/sidebars/projects/menus/wiki_menu.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ class WikiMenu < ::Sidebars::Menu
+ override :link
+ def link
+ wiki_path(context.project.wiki)
+ end
+
+ override :extra_container_html_options
+ def extra_container_html_options
+ {
+ class: 'shortcuts-wiki'
+ }
+ end
+
+ override :title
+ def title
+ _('Wiki')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'book'
+ end
+
+ override :render?
+ def render?
+ can?(context.current_user, :read_wiki, context.project)
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :wikis }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/projects/panel.rb b/lib/sidebars/projects/panel.rb
index f8320fe1acf..e489bcae7a8 100644
--- a/lib/sidebars/projects/panel.rb
+++ b/lib/sidebars/projects/panel.rb
@@ -20,6 +20,8 @@ module Sidebars
add_menu(Sidebars::Projects::Menus::InfrastructureMenu.new(context))
add_menu(Sidebars::Projects::Menus::PackagesRegistriesMenu.new(context))
add_menu(Sidebars::Projects::Menus::AnalyticsMenu.new(context))
+ add_menu(confluence_or_wiki_menu)
+ add_menu(Sidebars::Projects::Menus::ExternalWikiMenu.new(context))
end
override :render_raw_menus_partial
@@ -31,6 +33,14 @@ module Sidebars
def aria_label
_('Project navigation')
end
+
+ private
+
+ def confluence_or_wiki_menu
+ confluence_menu = ::Sidebars::Projects::Menus::ConfluenceMenu.new(context)
+
+ confluence_menu.render? ? confluence_menu : Sidebars::Projects::Menus::WikiMenu.new(context)
+ end
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 45cb69a93f4..70cba0de49f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5040,6 +5040,15 @@ msgstr ""
msgid "BillingPlan|Upgrade for free"
msgstr ""
+msgid "Billings|Verify User Account"
+msgstr ""
+
+msgid "Billings|Verify account"
+msgstr ""
+
+msgid "Billings|Your user account has been flagged for potential abuse for running a large number of concurrent pipelines. To continue to run a large number of concurrent pipelines, you'll need to validate your account with a credit card. %{strongStart}GitLab will not charge your credit card, it will only be used for validation.%{strongEnd}"
+msgstr ""
+
msgid "Billing|An email address is only visible for users with public emails."
msgstr ""
@@ -22984,9 +22993,6 @@ msgstr ""
msgid "Otherwise, click the link below to complete the process:"
msgstr ""
-msgid "Our documentation includes an example DevOps Score report."
-msgstr ""
-
msgid "Out-of-compliance with this project's policies and should be removed"
msgstr ""
@@ -28804,6 +28810,9 @@ msgstr ""
msgid "SecurityReports|Your feedback is important to us! We will ask again in a week."
msgstr ""
+msgid "See example DevOps Score page in our documentation."
+msgstr ""
+
msgid "See metrics"
msgstr ""
@@ -37181,6 +37190,9 @@ msgstr ""
msgid "Your authorized applications"
msgstr ""
+msgid "Your browser does not support iFrames"
+msgstr ""
+
msgid "Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer)."
msgstr ""
@@ -38255,9 +38267,6 @@ msgstr ""
msgid "mrWidget|Are you adding technical debt or code vulnerabilities?"
msgstr ""
-msgid "mrWidget|Before this can be merged, one or more threads must be resolved."
-msgstr ""
-
msgid "mrWidget|Cancel automatic merge"
msgstr ""
@@ -38324,6 +38333,9 @@ msgstr ""
msgid "mrWidget|Merge"
msgstr ""
+msgid "mrWidget|Merge blocked: all threads must be resolved."
+msgstr ""
+
msgid "mrWidget|Merge failed."
msgstr ""
diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb
index 71fe4c55d2c..0534157193d 100644
--- a/qa/qa/page/project/menu.rb
+++ b/qa/qa/page/project/menu.rb
@@ -18,10 +18,6 @@ module QA
element :members_link
end
- view 'app/views/layouts/nav/sidebar/_wiki_link.html.haml' do
- element :wiki_link
- end
-
def click_merge_requests
within_sidebar do
click_element(:sidebar_menu_link, menu_item: 'Merge requests')
@@ -30,7 +26,7 @@ module QA
def click_wiki
within_sidebar do
- click_element(:wiki_link)
+ click_element(:sidebar_menu_link, menu_item: 'Wiki')
end
end
diff --git a/spec/controllers/groups/autocomplete_sources_controller_spec.rb b/spec/controllers/groups/autocomplete_sources_controller_spec.rb
new file mode 100644
index 00000000000..6e814bebf39
--- /dev/null
+++ b/spec/controllers/groups/autocomplete_sources_controller_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::AutocompleteSourcesController do
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:group) { create(:group, :private) }
+
+ before_all do
+ group.add_developer(user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ describe '#issues' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:incident) { create(:incident, project: project) }
+
+ let(:none) { [] }
+ let(:all) { [issue, incident] }
+
+ where(:issue_types, :expected) do
+ nil | :all
+ '' | :all
+ 'invalid' | :none
+ 'issue' | :issue
+ 'incident' | :incident
+ end
+
+ with_them do
+ it 'returns the correct response', :aggregate_failures do
+ issues = Array(expected).flat_map { |sym| public_send(sym) }
+ params = { group_id: group, issue_types: issue_types }.compact
+
+ get :issues, params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an(Array)
+ expect(json_response.size).to eq(issues.size)
+ expect(json_response.map { |issue| issue['iid'] })
+ .to match_array(issues.map(&:iid))
+ end
+ end
+ end
+
+ describe '#milestones' do
+ it 'returns correct response' do
+ parent_group = create(:group, :private)
+ group.update!(parent: parent_group)
+ sub_group = create(:group, :private, parent: sub_group)
+ create(:milestone, group: parent_group)
+ create(:milestone, group: sub_group)
+ group_milestone = create(:milestone, group: group)
+
+ get :milestones, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first).to include(
+ 'iid' => group_milestone.iid, 'title' => group_milestone.title
+ )
+ end
+ end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index f8bd0ffff64..a2bc204ad0b 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -369,6 +369,12 @@ FactoryBot.define do
end
end
+ trait :codequality_reports_without_degradation do
+ after(:build) do |build|
+ build.job_artifacts << create(:ci_job_artifact, :codequality_without_errors, job: build)
+ end
+ end
+
trait :terraform_reports do
after(:build) do |build|
build.job_artifacts << create(:ci_job_artifact, :terraform, job: build)
diff --git a/spec/features/groups/members/manage_groups_spec.rb b/spec/features/groups/members/manage_groups_spec.rb
index e9bbe9de3c9..3b56305f711 100644
--- a/spec/features/groups/members/manage_groups_spec.rb
+++ b/spec/features/groups/members/manage_groups_spec.rb
@@ -12,18 +12,39 @@ RSpec.describe 'Groups > Members > Manage groups', :js do
sign_in(user)
end
- context 'when group link does not exist' do
- let_it_be(:group) { create(:group) }
- let_it_be(:group_to_add) { create(:group) }
+ context 'with invite_members_group_modal disabled' do
+ context 'when group link does not exist' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:group_to_add) { create(:group) }
- before do
- stub_feature_flags(invite_members_group_modal: false)
- group.add_owner(user)
- visit group_group_members_path(group)
+ before do
+ stub_feature_flags(invite_members_group_modal: false)
+ group.add_owner(user)
+ visit group_group_members_path(group)
+ end
+
+ it 'add group to group' do
+ add_group(group_to_add.id, 'Reporter')
+
+ click_groups_tab
+
+ page.within(first_row) do
+ expect(page).to have_content(group_to_add.name)
+ expect(page).to have_content('Reporter')
+ end
+ end
end
+ end
- it 'add group to group' do
- add_group(group_to_add.id, 'Reporter')
+ context 'when group link does not exist' do
+ it 'can share a group with group' do
+ group = create(:group)
+ group_to_add = create(:group)
+ group.add_owner(user)
+ group_to_add.add_owner(user)
+
+ visit group_group_members_path(group)
+ invite_group(group_to_add.name, 'Reporter')
click_groups_tab
@@ -126,6 +147,21 @@ RSpec.describe 'Groups > Members > Manage groups', :js do
end
end
+ def invite_group(name, role)
+ click_on 'Invite a group'
+
+ click_on 'Select a group'
+ wait_for_requests
+ click_button name
+
+ click_button 'Guest'
+ wait_for_requests
+ click_button role
+
+ click_button 'Invite'
+ page.refresh
+ end
+
def click_groups_tab
expect(page).to have_link 'Groups'
click_link "Groups"
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 1d9ac5ee1e9..c51ee250331 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -54,11 +54,11 @@ RSpec.describe 'Group milestones' do
expect(find('.start_date')).to have_content(Date.today.at_beginning_of_month.strftime('%b %-d, %Y'))
end
- it 'description input does not support autocomplete' do
+ it 'description input support autocomplete' do
description = find('.note-textarea')
description.native.send_keys('!')
- expect(page).not_to have_selector('.atwho-view')
+ expect(page).to have_selector('.atwho-view')
end
end
diff --git a/spec/features/groups/milestones/gfm_autocomplete_spec.rb b/spec/features/groups/milestones/gfm_autocomplete_spec.rb
new file mode 100644
index 00000000000..85a14123294
--- /dev/null
+++ b/spec/features/groups/milestones/gfm_autocomplete_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'GFM autocomplete', :js do
+ let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
+ let_it_be(:group) { create(:group, name: 'Ancestor') }
+ let_it_be(:project) { create(:project, :repository, group: group) }
+ let_it_be(:issue) { create(:issue, project: project, assignees: [user], title: 'My special issue') }
+ let_it_be(:label) { create(:group_label, group: group, title: 'special+') }
+ let_it_be(:milestone) { create(:milestone, resource_parent: group, title: "group milestone") }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+
+ shared_examples 'displays autocomplete menu for all entities' do
+ it 'autocompletes all available entities' do
+ fill_in 'Description', with: User.reference_prefix
+ wait_for_requests
+ expect(find_autocomplete_menu).to be_visible
+ expect_autocomplete_entry(group.name)
+
+ fill_in 'Description', with: Label.reference_prefix
+ wait_for_requests
+ expect(find_autocomplete_menu).to be_visible
+ expect_autocomplete_entry(label.title)
+
+ fill_in 'Description', with: Milestone.reference_prefix
+ wait_for_requests
+ expect(find_autocomplete_menu).to be_visible
+ expect_autocomplete_entry(milestone.title)
+
+ fill_in 'Description', with: Issue.reference_prefix
+ wait_for_requests
+ expect(find_autocomplete_menu).to be_visible
+ expect_autocomplete_entry(issue.title)
+
+ fill_in 'Description', with: MergeRequest.reference_prefix
+ wait_for_requests
+ expect(find_autocomplete_menu).to be_visible
+ expect_autocomplete_entry(merge_request.title)
+ end
+ end
+
+ before_all do
+ group.add_maintainer(user)
+ end
+
+ describe 'new milestone page' do
+ before do
+ sign_in(user)
+ visit new_group_milestone_path(group)
+
+ wait_for_requests
+ end
+
+ it_behaves_like 'displays autocomplete menu for all entities'
+ end
+
+ describe 'update milestone page' do
+ before do
+ sign_in(user)
+ visit edit_group_milestone_path(group, milestone)
+
+ wait_for_requests
+ end
+
+ it_behaves_like 'displays autocomplete menu for all entities'
+ end
+
+ private
+
+ def find_autocomplete_menu
+ find('.atwho-view ul', visible: true)
+ end
+
+ def expect_autocomplete_entry(entry)
+ page.within('.atwho-container') do
+ expect(page).to have_content(entry)
+ end
+ end
+end
diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
index 34d78880991..a4c0a84af7d 100644
--- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
+++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe 'Resolving all open threads in a merge request from an issue', :j
end
it 'shows a warning that the merge request contains unresolved threads' do
- expect(page).to have_content 'Before this can be merged,'
+ expect(page).to have_content 'all threads must be resolved'
end
it 'has a link to resolve all threads by creating an issue' do
diff --git a/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
index ac38b2b854c..e250837f398 100644
--- a/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_button_depending_on_unresolved_discussions_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe 'Merge request > User sees merge button depending on unresolved t
context 'with unresolved threads' do
it 'does not allow to merge' do
expect(page).not_to have_button 'Merge'
- expect(page).to have_content('Before this can be merged,')
+ expect(page).to have_content('all threads must be resolved')
end
end
diff --git a/spec/features/projects/diffs/diff_show_spec.rb b/spec/features/projects/diffs/diff_show_spec.rb
index 747277e2562..e47f36c4b7a 100644
--- a/spec/features/projects/diffs/diff_show_spec.rb
+++ b/spec/features/projects/diffs/diff_show_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Diff file viewer', :js do
+RSpec.describe 'Diff file viewer', :js, :with_clean_rails_cache do
let(:project) { create(:project, :public, :repository) }
def visit_commit(sha, anchor: nil)
diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb
index 83ba2533a73..d728c7b1a73 100644
--- a/spec/features/projects/members/invite_group_spec.rb
+++ b/spec/features/projects/members/invite_group_spec.rb
@@ -9,22 +9,44 @@ RSpec.describe 'Project > Members > Invite group', :js do
let(:maintainer) { create(:user) }
- before do
- stub_feature_flags(invite_members_group_modal: false)
+ using RSpec::Parameterized::TableSyntax
+
+ where(:invite_members_group_modal_enabled, :expected_invite_group_selector) do
+ true | 'button[data-qa-selector="invite_a_group_button"]'
+ false | '#invite-group-tab'
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(invite_members_group_modal: invite_members_group_modal_enabled)
+ end
+
+ it 'displays either the invite group button or the form with tabs based on the feature flag' do
+ project = create(:project, namespace: create(:group))
+
+ project.add_maintainer(maintainer)
+ sign_in(maintainer)
+
+ visit project_project_members_path(project)
+
+ expect(page).to have_selector(expected_invite_group_selector)
+ end
end
describe 'Share with group lock' do
+ let(:invite_group_selector) { 'button[data-qa-selector="invite_a_group_button"]' }
+
shared_examples 'the project can be shared with groups' do
- it 'the "Invite group" tab exists' do
+ it 'the "Invite a group" button exists' do
visit project_project_members_path(project)
- expect(page).to have_selector('#invite-group-tab')
+ expect(page).to have_selector(invite_group_selector)
end
end
shared_examples 'the project cannot be shared with groups' do
- it 'the "Invite group" tab does not exist' do
+ it 'the "Invite a group" button does not exist' do
visit project_project_members_path(project)
- expect(page).not_to have_selector('#invite-group-tab')
+ expect(page).not_to have_selector(invite_group_selector)
end
end
@@ -41,7 +63,9 @@ RSpec.describe 'Project > Members > Invite group', :js do
context 'when the group has "Share with group lock" disabled' do
it_behaves_like 'the project can be shared with groups'
- it 'the project can be shared with another group' do
+ it 'the project can be shared with another group when the feature flag invite_members_group_modal is disabled' do
+ stub_feature_flags(invite_members_group_modal: false)
+
visit project_project_members_path(project)
expect(page).not_to have_link 'Groups'
@@ -56,6 +80,27 @@ RSpec.describe 'Project > Members > Invite group', :js do
expect(members_table).to have_content(group_to_share_with.name)
end
+
+ it 'the project can be shared with another group when the feature flag invite_members_group_modal is enabled' do
+ stub_feature_flags(invite_members_group_modal: true)
+
+ visit project_project_members_path(project)
+
+ expect(page).not_to have_link 'Groups'
+
+ click_on 'Invite a group'
+
+ click_on 'Select a group'
+ wait_for_requests
+ click_button group_to_share_with.name
+ click_button 'Invite'
+
+ visit project_project_members_path(project)
+
+ click_link 'Groups'
+
+ expect(members_table).to have_content(group_to_share_with.name)
+ end
end
context 'when the group has "Share with group lock" enabled' do
@@ -127,13 +172,14 @@ RSpec.describe 'Project > Members > Invite group', :js do
visit project_project_members_path(project)
- click_on 'invite-group-tab'
+ click_on 'Invite a group'
+ click_on 'Select a group'
+ wait_for_requests
+ click_button group.name
+ fill_in 'YYYY-MM-DD', with: 5.days.from_now.strftime('%Y-%m-%d')
+ click_button 'Invite'
- select2 group.id, from: '#link_group_id'
-
- fill_in 'expires_at_groups', with: 5.days.from_now.strftime('%Y-%m-%d')
- click_on 'invite-group-tab'
- find('.btn-confirm').click
+ page.refresh
end
it 'the group link shows the expiration time with a warning class' do
@@ -149,29 +195,23 @@ RSpec.describe 'Project > Members > Invite group', :js do
context 'with multiple groups to choose from' do
let(:project) { create(:project) }
- before do
+ it 'includes multiple groups' do
project.add_maintainer(maintainer)
sign_in(maintainer)
- create(:group).add_owner(maintainer)
- create(:group).add_owner(maintainer)
+ group1 = create(:group)
+ group1.add_owner(maintainer)
+ group2 = create(:group)
+ group2.add_owner(maintainer)
visit project_project_members_path(project)
- click_link 'Invite group'
+ click_on 'Invite a group'
+ click_on 'Select a group'
+ wait_for_requests
- find('.ajax-groups-select.select2-container')
-
- execute_script 'GROUP_SELECT_PER_PAGE = 1;'
- open_select2 '#link_group_id'
- end
-
- it 'infinitely scrolls' do
- expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
-
- scroll_select2_to_bottom('.select2-drop .select2-results:visible')
-
- expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
+ expect(page).to have_button(group1.name)
+ expect(page).to have_button(group2.name)
end
end
@@ -188,16 +228,19 @@ RSpec.describe 'Project > Members > Invite group', :js do
group_to_share_with.add_maintainer(maintainer)
end
- it 'the groups dropdown does not show ancestors' do
+ # This behavior should be changed to exclude the ancestor and project
+ # group from the options once issue is fixed for the modal:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/329835
+ it 'the groups dropdown does show ancestors and the project group' do
visit project_project_members_path(project)
- click_on 'invite-group-tab'
- click_link 'Search for a group'
+ click_on 'Invite a group'
+ click_on 'Select a group'
+ wait_for_requests
- page.within '.select2-drop' do
- expect(page).to have_content(group_to_share_with.name)
- expect(page).not_to have_content(group.name)
- end
+ expect(page).to have_button(group_to_share_with.name)
+ expect(page).to have_button(group.name)
+ expect(page).to have_button(nested_group.name)
end
end
end
diff --git a/spec/features/projects/milestones/gfm_autocomplete_spec.rb b/spec/features/projects/milestones/gfm_autocomplete_spec.rb
new file mode 100644
index 00000000000..547a5d11dec
--- /dev/null
+++ b/spec/features/projects/milestones/gfm_autocomplete_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'GFM autocomplete', :js do
+ let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
+ let_it_be(:group) { create(:group, name: 'Ancestor') }
+ let_it_be(:project) { create(:project, :repository, group: group) }
+ let_it_be(:issue) { create(:issue, project: project, assignees: [user], title: 'My special issue') }
+ let_it_be(:label) { create(:label, project: project, title: 'special+') }
+ let_it_be(:milestone) { create(:milestone, resource_parent: project, title: "project milestone") }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+
+ shared_examples 'displays autocomplete menu for all entities' do
+ it 'autocompletes all available entities' do
+ fill_in 'Description', with: User.reference_prefix
+ wait_for_requests
+ expect(find_autocomplete_menu).to be_visible
+ expect_autocomplete_entry(user.name)
+
+ fill_in 'Description', with: Label.reference_prefix
+ wait_for_requests
+ expect(find_autocomplete_menu).to be_visible
+ expect_autocomplete_entry(label.title)
+
+ fill_in 'Description', with: Milestone.reference_prefix
+ wait_for_requests
+ expect(find_autocomplete_menu).to be_visible
+ expect_autocomplete_entry(milestone.title)
+
+ fill_in 'Description', with: Issue.reference_prefix
+ wait_for_requests
+ expect(find_autocomplete_menu).to be_visible
+ expect_autocomplete_entry(issue.title)
+
+ fill_in 'Description', with: MergeRequest.reference_prefix
+ wait_for_requests
+ expect(find_autocomplete_menu).to be_visible
+ expect_autocomplete_entry(merge_request.title)
+ end
+ end
+
+ before_all do
+ group.add_maintainer(user)
+ end
+
+ describe 'new milestone page' do
+ before do
+ sign_in(user)
+ visit new_project_milestone_path(project)
+
+ wait_for_requests
+ end
+
+ it_behaves_like 'displays autocomplete menu for all entities'
+ end
+
+ describe 'update milestone page' do
+ before do
+ sign_in(user)
+ visit edit_project_milestone_path(project, milestone)
+
+ wait_for_requests
+ end
+
+ it_behaves_like 'displays autocomplete menu for all entities'
+ end
+
+ private
+
+ def find_autocomplete_menu
+ find('.atwho-view ul', visible: true)
+ end
+
+ def expect_autocomplete_entry(entry)
+ page.within('.atwho-container') do
+ expect(page).to have_content(entry)
+ end
+ end
+end
diff --git a/spec/fixtures/pipeline_artifacts/code_quality_mr_diff.json b/spec/fixtures/pipeline_artifacts/code_quality_mr_diff.json
index c3ee2bc4cac..5489330fc1d 100644
--- a/spec/fixtures/pipeline_artifacts/code_quality_mr_diff.json
+++ b/spec/fixtures/pipeline_artifacts/code_quality_mr_diff.json
@@ -1,23 +1,25 @@
{
- "files": {
- "file_a.rb": [
- {
- "line": 10,
- "description": "Avoid parameter lists longer than 5 parameters. [12/5]",
- "severity": "major"
- },
- {
- "line": 10,
- "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
- "severity": "minor"
- }
- ],
- "file_b.rb": [
- {
- "line": 10,
- "description": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count.",
- "severity": "minor"
- }
- ]
+ "merge_request_123456789": {
+ "files": {
+ "file_a.rb": [
+ {
+ "line": 10,
+ "description": "Avoid parameter lists longer than 5 parameters. [12/5]",
+ "severity": "major"
+ },
+ {
+ "line": 10,
+ "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
+ "severity": "minor"
+ }
+ ],
+ "file_b.rb": [
+ {
+ "line": 10,
+ "description": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count.",
+ "severity": "minor"
+ }
+ ]
+ }
}
}
diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
index 09979694e07..7c20bbe21c8 100644
--- a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
+++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
@@ -1,89 +1,132 @@
-import { GlTable, GlBadge } from '@gitlab/ui';
+import { GlTable, GlBadge, GlEmptyState, GlLink } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import DevopsScore from '~/analytics/devops_report/components/devops_score.vue';
-import { createdAt, cards, averageScore, devopsScoreTableHeaders } from '../mock_data';
+import {
+ devopsScoreMetricsData,
+ devopsReportDocsPath,
+ noDataImagePath,
+ devopsScoreTableHeaders,
+} from '../mock_data';
describe('DevopsScore', () => {
let wrapper;
- const createComponent = () => {
+ const createComponent = ({ devopsScoreMetrics = devopsScoreMetricsData } = {}) => {
wrapper = extendedWrapper(
mount(DevopsScore, {
provide: {
- devopsScoreMetrics: {
- createdAt,
- cards,
- averageScore,
- },
+ devopsScoreMetrics,
+ devopsReportDocsPath,
+ noDataImagePath,
},
}),
);
};
- const findTable = () => wrapper.find(GlTable);
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findCol = (testId) => findTable().find(`[data-testid="${testId}"]`);
const findUsageCol = () => findCol('usageCol');
+ const findDevopsScoreApp = () => wrapper.findByTestId('devops-score-app');
- beforeEach(() => {
- createComponent();
- });
-
- it('displays the title note', () => {
- expect(wrapper.findByTestId('devops-score-note-text').text()).toBe(
- 'DevOps score metrics are based on usage over the last 30 days. Last updated: 2020-06-29 08:16.',
- );
- });
-
- it('displays the single stat section', () => {
- const component = wrapper.find(GlSingleStat);
-
- expect(component.exists()).toBe(true);
- expect(component.props('value')).toBe(averageScore.value);
- });
-
- describe('devops score table', () => {
- it('displays the table', () => {
- expect(findTable().exists()).toBe(true);
+ describe('with no data', () => {
+ beforeEach(() => {
+ createComponent({ devopsScoreMetrics: {} });
});
- describe('table headings', () => {
- let headers;
-
- beforeEach(() => {
- headers = findTable().findAll("[data-testid='header']");
+ describe('empty state', () => {
+ it('displays the empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
});
- it('displays the correct number of headings', () => {
- expect(headers).toHaveLength(devopsScoreTableHeaders.length);
+ it('displays the correct message', () => {
+ expect(findEmptyState().text()).toBe(
+ 'Data is still calculating... It may be several days before you see feature usage data. See example DevOps Score page in our documentation.',
+ );
});
- describe.each(devopsScoreTableHeaders)('header fields', ({ label, index }) => {
- let headerWrapper;
-
- beforeEach(() => {
- headerWrapper = headers.at(index);
- });
-
- it(`displays the correct table heading text for "${label}"`, () => {
- expect(headerWrapper.text()).toContain(label);
- });
+ it('contains a link to the feature documentation', () => {
+ expect(wrapper.findComponent(GlLink).exists()).toBe(true);
});
});
- describe('table columns', () => {
- describe('Your usage', () => {
- it('displays the corrrect value', () => {
- expect(findUsageCol().text()).toContain('3.2');
+ it('does not display the devops score app', () => {
+ expect(findDevopsScoreApp().exists()).toBe(false);
+ });
+ });
+
+ describe('with data', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not display the empty state', () => {
+ expect(findEmptyState().exists()).toBe(false);
+ });
+
+ it('displays the devops score app', () => {
+ expect(findDevopsScoreApp().exists()).toBe(true);
+ });
+
+ describe('devops score app', () => {
+ it('displays the title note', () => {
+ expect(wrapper.findByTestId('devops-score-note-text').text()).toBe(
+ 'DevOps score metrics are based on usage over the last 30 days. Last updated: 2020-06-29 08:16.',
+ );
+ });
+
+ it('displays the single stat section', () => {
+ const component = wrapper.findComponent(GlSingleStat);
+
+ expect(component.exists()).toBe(true);
+ expect(component.props('value')).toBe(devopsScoreMetricsData.averageScore.value);
+ });
+
+ describe('devops score table', () => {
+ it('displays the table', () => {
+ expect(findTable().exists()).toBe(true);
});
- it('displays the corrrect badge', () => {
- const badge = findUsageCol().find(GlBadge);
+ describe('table headings', () => {
+ let headers;
- expect(badge.exists()).toBe(true);
- expect(badge.props('variant')).toBe('muted');
- expect(badge.text()).toBe('Low');
+ beforeEach(() => {
+ headers = findTable().findAll("[data-testid='header']");
+ });
+
+ it('displays the correct number of headings', () => {
+ expect(headers).toHaveLength(devopsScoreTableHeaders.length);
+ });
+
+ describe.each(devopsScoreTableHeaders)('header fields', ({ label, index }) => {
+ let headerWrapper;
+
+ beforeEach(() => {
+ headerWrapper = headers.at(index);
+ });
+
+ it(`displays the correct table heading text for "${label}"`, () => {
+ expect(headerWrapper.text()).toContain(label);
+ });
+ });
+ });
+
+ describe('table columns', () => {
+ describe('Your usage', () => {
+ it('displays the corrrect value', () => {
+ expect(findUsageCol().text()).toContain('3.2');
+ });
+
+ it('displays the corrrect badge', () => {
+ const badge = findUsageCol().find(GlBadge);
+
+ expect(badge.exists()).toBe(true);
+ expect(badge.props('variant')).toBe('muted');
+ expect(badge.text()).toBe('Low');
+ });
+ });
});
});
});
diff --git a/spec/frontend/admin/analytics/devops_score/mock_data.js b/spec/frontend/admin/analytics/devops_score/mock_data.js
index 358568c6e39..ae0c01a2661 100644
--- a/spec/frontend/admin/analytics/devops_score/mock_data.js
+++ b/spec/frontend/admin/analytics/devops_score/mock_data.js
@@ -1,27 +1,3 @@
-export const averageScore = {
- value: '10',
- scoreLevel: {
- label: 'High',
- icon: 'check-circle',
- variant: 'success',
- },
-};
-
-export const cards = [
- {
- title: 'Issues created per active user',
- usage: '3.2',
- leadInstance: '10.2',
- score: '0',
- scoreLevel: {
- label: 'Low',
- variant: 'muted',
- },
- },
-];
-
-export const createdAt = '2020-06-29 08:16';
-
export const devopsScoreTableHeaders = [
{
index: 0,
@@ -40,3 +16,31 @@ export const devopsScoreTableHeaders = [
label: 'Score',
},
];
+
+export const devopsScoreMetricsData = {
+ createdAt: '2020-06-29 08:16',
+ cards: [
+ {
+ title: 'Issues created per active user',
+ usage: '3.2',
+ leadInstance: '10.2',
+ score: '0',
+ scoreLevel: {
+ label: 'Low',
+ variant: 'muted',
+ },
+ },
+ ],
+ averageScore: {
+ value: '10',
+ scoreLevel: {
+ label: 'High',
+ icon: 'check-circle',
+ variant: 'success',
+ },
+ },
+};
+
+export const devopsReportDocsPath = 'docs-path';
+
+export const noDataImagePath = 'image-path';
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
index 6c0d69ea109..c6bfca4516f 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
@@ -42,9 +42,7 @@ describe('UnresolvedDiscussions', () => {
});
it('should have correct elements', () => {
- expect(wrapper.element.innerText).toContain(
- `Before this can be merged, one or more threads must be resolved.`,
- );
+ expect(wrapper.element.innerText).toContain(`Merge blocked: all threads must be resolved.`);
expect(wrapper.element.innerText).toContain('Jump to first unresolved thread');
expect(wrapper.element.innerText).toContain('Resolve all threads in new issue');
@@ -56,9 +54,7 @@ describe('UnresolvedDiscussions', () => {
describe('without threads path', () => {
it('should not show create issue link if user cannot create issue', () => {
- expect(wrapper.element.innerText).toContain(
- `Before this can be merged, one or more threads must be resolved.`,
- );
+ expect(wrapper.element.innerText).toContain(`Merge blocked: all threads must be resolved.`);
expect(wrapper.element.innerText).toContain('Jump to first unresolved thread');
expect(wrapper.element.innerText).not.toContain('Resolve all threads in new issue');
diff --git a/spec/graphql/types/timelog_type_spec.rb b/spec/graphql/types/timelog_type_spec.rb
index 38bd70d5097..791c2fdb046 100644
--- a/spec/graphql/types/timelog_type_spec.rb
+++ b/spec/graphql/types/timelog_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['Timelog'] do
- let(:fields) { %i[spent_at time_spent user issue note] }
+ let(:fields) { %i[spent_at time_spent user issue merge_request note] }
it { expect(described_class.graphql_name).to eq('Timelog') }
it { expect(described_class).to have_graphql_fields(fields) }
@@ -25,6 +25,14 @@ RSpec.describe GitlabSchema.types['Timelog'] do
end
end
+ describe 'merge_request field' do
+ subject { described_class.fields['mergeRequest'] }
+
+ it 'returns merge_request' do
+ is_expected.to have_graphql_type(Types::MergeRequestType)
+ end
+ end
+
describe 'note field' do
subject { described_class.fields['note'] }
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index ae039c1a8b1..bf533ca7034 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -284,14 +284,29 @@ RSpec.describe ApplicationHelper do
end
describe '#autocomplete_data_sources' do
- let(:project) { create(:project) }
- let(:noteable_type) { Issue }
+ context 'group' do
+ let(:group) { create(:group) }
+ let(:noteable_type) { Issue }
- it 'returns paths for autocomplete_sources_controller' do
- sources = helper.autocomplete_data_sources(project, noteable_type)
- expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets])
- sources.keys.each do |key|
- expect(sources[key]).not_to be_nil
+ it 'returns paths for autocomplete_sources_controller' do
+ sources = helper.autocomplete_data_sources(group, noteable_type)
+ expect(sources.keys).to include(:members, :issues, :mergeRequests, :labels, :milestones, :commands)
+ sources.keys.each do |key|
+ expect(sources[key]).not_to be_nil
+ end
+ end
+ end
+
+ context 'project' do
+ let(:project) { create(:project) }
+ let(:noteable_type) { Issue }
+
+ it 'returns paths for autocomplete_sources_controller' do
+ sources = helper.autocomplete_data_sources(project, noteable_type)
+ expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets])
+ sources.keys.each do |key|
+ expect(sources[key]).not_to be_nil
+ end
end
end
end
diff --git a/spec/helpers/dev_ops_report_helper_spec.rb b/spec/helpers/dev_ops_report_helper_spec.rb
index 769400521f8..7e7a89a3039 100644
--- a/spec/helpers/dev_ops_report_helper_spec.rb
+++ b/spec/helpers/dev_ops_report_helper_spec.rb
@@ -31,5 +31,11 @@ RSpec.describe DevOpsReportHelper do
it { expect(devops_score_metrics[:averageScore]).to eq({ scoreLevel: { icon: "status_success_solid", label: "High", variant: "success" }, value: "82.0" } ) }
end
+
+ describe 'with blank metrics' do
+ let(:devops_score_metrics) { helper.devops_score_metrics({}) }
+
+ it { expect(devops_score_metrics).to eq({}) }
+ end
end
end
diff --git a/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb b/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb
index 8b177fa7fc1..73b916da2e9 100644
--- a/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb
+++ b/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb
@@ -9,14 +9,11 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityMrDiff do
let(:degradation_3) { build(:codequality_degradation_3) }
describe '#initialize!' do
- subject(:report) { described_class.new(codequality_report) }
+ subject(:report) { described_class.new(new_degradations) }
context 'when quality has degradations' do
context 'with several degradations on the same line' do
- before do
- codequality_report.add_degradation(degradation_1)
- codequality_report.add_degradation(degradation_2)
- end
+ let(:new_degradations) { [degradation_1, degradation_2] }
it 'generates quality report for mr diff' do
expect(report.files).to match(
@@ -29,11 +26,7 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityMrDiff do
end
context 'with several degradations on several files' do
- before do
- codequality_report.add_degradation(degradation_1)
- codequality_report.add_degradation(degradation_2)
- codequality_report.add_degradation(degradation_3)
- end
+ let(:new_degradations) { [degradation_1, degradation_2, degradation_3] }
it 'returns quality report for mr diff' do
expect(report.files).to match(
@@ -50,6 +43,8 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityMrDiff do
end
context 'when quality has no degradation' do
+ let(:new_degradations) { [] }
+
it 'returns an empty hash' do
expect(report.files).to match({})
end
diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb
index 8378d096fcf..e289e59b281 100644
--- a/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb
+++ b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do
let(:base_report) { Gitlab::Ci::Reports::CodequalityReports.new }
let(:head_report) { Gitlab::Ci::Reports::CodequalityReports.new }
let(:major_degradation) { build(:codequality_degradation, :major) }
- let(:minor_degradation) { build(:codequality_degradation, :major) }
+ let(:minor_degradation) { build(:codequality_degradation, :minor) }
let(:critical_degradation) { build(:codequality_degradation, :critical) }
let(:blocker_degradation) { build(:codequality_degradation, :blocker) }
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index 32ca6e4fde6..94d3b2ad0b3 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Diff::Highlight do
include RepoHelpers
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
+
let(:commit) { project.commit(sample_commit.id) }
let(:diff) { commit.raw_diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
@@ -156,5 +157,34 @@ RSpec.describe Gitlab::Diff::Highlight do
it_behaves_like 'without inline diffs'
end
end
+
+ context 'when blob is too large' do
+ let(:subject) { described_class.new(diff_file, repository: project.repository).highlight }
+
+ before do
+ allow(Gitlab::Highlight).to receive(:too_large?).and_return(true)
+ end
+
+ it 'blobs are highlighted as plain text without loading all data' do
+ expect(diff_file.blob).not_to receive(:load_all_data!)
+
+ expect(subject[2].rich_text).to eq(%Q{ def popen(cmd, path=nil)\n})
+ expect(subject[2].rich_text).to be_html_safe
+ end
+
+ context 'when limited_diff_highlighting is disabled' do
+ before do
+ stub_feature_flags(limited_diff_highlighting: false)
+ stub_feature_flags(diff_line_syntax_highlighting: false)
+ end
+
+ it 'blobs are highlighted as plain text with loading all data' do
+ expect(diff_file.blob).to receive(:load_all_data!).twice
+
+ code = %Q{ def popen(cmd, path=nil)\n}
+ expect(subject[2].rich_text).to eq(code)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index 0ea974921bc..cf0d1577314 100644
--- a/spec/lib/gitlab/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -216,4 +216,63 @@ RSpec.describe Gitlab::EncodingHelper do
expect(test).not_to eq(io_stream)
end
end
+
+ describe '#detect_encoding' do
+ subject { ext_class.detect_encoding(data, **kwargs) }
+
+ let(:data) { binary_string }
+ let(:kwargs) { {} }
+
+ shared_examples 'detects encoding' do
+ it { is_expected.to be_a(Hash) }
+
+ it 'correctly detects the binary' do
+ expect(subject[:type]).to eq(:binary)
+ end
+
+ context 'data is nil' do
+ let(:data) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'limit is provided' do
+ let(:kwargs) do
+ { limit: 10 }
+ end
+
+ it 'correctly detects the binary' do
+ expect(subject[:type]).to eq(:binary)
+ end
+ end
+ end
+
+ context 'cached_encoding_detection is enabled' do
+ before do
+ stub_feature_flags(cached_encoding_detection: true)
+ end
+
+ it_behaves_like 'detects encoding'
+
+ context 'cache_key is provided' do
+ let(:kwargs) do
+ { cache_key: %w(foo bar) }
+ end
+
+ it 'uses that cache_key to serve from the cache' do
+ expect(Rails.cache).to receive(:fetch).with([:detect_binary, CharlockHolmes::VERSION, %w(foo bar)], expires_in: 1.week).and_call_original
+
+ expect(subject[:type]).to eq(:binary)
+ end
+ end
+ end
+
+ context 'cached_encoding_detection is disabled' do
+ before do
+ stub_feature_flags(cached_encoding_detection: false)
+ end
+
+ it_behaves_like 'detects encoding'
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
index 67d7b37dd45..495cb16ebab 100644
--- a/spec/lib/gitlab/git/blame_spec.rb
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -53,7 +53,10 @@ RSpec.describe Gitlab::Git::Blame, :seed_helper do
end
it 'converts to UTF-8' do
- expect(CharlockHolmes::EncodingDetector).to receive(:detect).and_return(nil)
+ expect_next_instance_of(CharlockHolmes::EncodingDetector) do |detector|
+ expect(detector).to receive(:detect).and_return(nil)
+ end
+
data = []
blame.each do |commit, line|
data << {
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 1e259c9c153..1ddbdda12b5 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -604,29 +604,29 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
.to receive(:find_remote_root_ref).and_call_original
- expect(repository.find_remote_root_ref('origin')).to eq 'master'
+ expect(repository.find_remote_root_ref('origin', SeedHelper::GITLAB_GIT_TEST_REPO_URL)).to eq 'master'
end
it 'returns UTF-8' do
- expect(repository.find_remote_root_ref('origin')).to be_utf8
+ expect(repository.find_remote_root_ref('origin', SeedHelper::GITLAB_GIT_TEST_REPO_URL)).to be_utf8
end
it 'returns nil when remote name is nil' do
expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
.not_to receive(:find_remote_root_ref)
- expect(repository.find_remote_root_ref(nil)).to be_nil
+ expect(repository.find_remote_root_ref(nil, nil)).to be_nil
end
it 'returns nil when remote name is empty' do
expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
.not_to receive(:find_remote_root_ref)
- expect(repository.find_remote_root_ref('')).to be_nil
+ expect(repository.find_remote_root_ref('', '')).to be_nil
end
it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RemoteService, :find_remote_root_ref do
- subject { repository.find_remote_root_ref('origin') }
+ subject { repository.find_remote_root_ref('origin', SeedHelper::GITLAB_GIT_TEST_REPO_URL) }
end
end
diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
index b9ef76e1f41..70fc4fe4416 100644
--- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
@@ -35,22 +35,50 @@ RSpec.describe Gitlab::GitalyClient::RemoteService do
end
describe '#find_remote_root_ref' do
- it 'sends an find_remote_root_ref message and returns the root ref' do
- expect_any_instance_of(Gitaly::RemoteService::Stub)
- .to receive(:find_remote_root_ref)
- .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
- .and_return(double(ref: 'master'))
+ let(:remote) { 'origin' }
+ let(:url) { 'http://git.example.com/my-repo.git' }
+ let(:auth) { 'Basic secret' }
- expect(client.find_remote_root_ref('origin')).to eq 'master'
+ shared_examples 'a find_remote_root_ref call' do
+ it 'sends an find_remote_root_ref message and returns the root ref' do
+ expect_any_instance_of(Gitaly::RemoteService::Stub)
+ .to receive(:find_remote_root_ref)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .with(gitaly_request_with_params(expected_params), kind_of(Hash))
+ .and_return(double(ref: 'master'))
+
+ expect(client.find_remote_root_ref(remote, url, auth)).to eq 'master'
+ end
+
+ it 'ensure ref is a valid UTF-8 string' do
+ expect_any_instance_of(Gitaly::RemoteService::Stub)
+ .to receive(:find_remote_root_ref)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .with(gitaly_request_with_params(expected_params), kind_of(Hash))
+ .and_return(double(ref: "an_invalid_ref_\xE5"))
+
+ expect(client.find_remote_root_ref(remote, url, auth)).to eq "an_invalid_ref_å"
+ end
end
- it 'ensure ref is a valid UTF-8 string' do
- expect_any_instance_of(Gitaly::RemoteService::Stub)
- .to receive(:find_remote_root_ref)
- .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
- .and_return(double(ref: "an_invalid_ref_\xE5"))
+ context 'with inmemory feature enabled' do
+ before do
+ stub_feature_flags(find_remote_root_refs_inmemory: true)
+ end
- expect(client.find_remote_root_ref('origin')).to eq "an_invalid_ref_å"
+ it_behaves_like 'a find_remote_root_ref call' do
+ let(:expected_params) { { remote_url: url, http_authorization_header: auth } }
+ end
+ end
+
+ context 'with inmemory feature disabled' do
+ before do
+ stub_feature_flags(find_remote_root_refs_inmemory: false)
+ end
+
+ it_behaves_like 'a find_remote_root_ref call' do
+ let(:expected_params) { { remote: remote } }
+ end
end
end
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index 5dcee1ff724..5ec66e7f6a8 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -133,5 +133,39 @@ RSpec.describe Gitlab::Highlight do
subject.highlight("Content")
end
end
+
+ describe 'highlight timeouts' do
+ context 'when there is a timeout error while highlighting' do
+ let(:result) { described_class.highlight(file_name, content) }
+
+ before do
+ allow(Timeout).to receive(:timeout).twice.and_raise(Timeout::Error)
+ # This is done twice because it's rescued first and then
+ # calls the original exception
+ end
+
+ it "increments the foreground counter if it's in the foreground" do
+ expect { result }
+ .to raise_error(Timeout::Error)
+ .and change { highlight_timeout_total('foreground') }.by(1)
+ .and not_change { highlight_timeout_total('background') }
+ end
+
+ it "increments the background counter if it's in the background" do
+ allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
+
+ expect { result }
+ .to raise_error(Timeout::Error)
+ .and change { highlight_timeout_total('background') }.by(1)
+ .and not_change { highlight_timeout_total('foreground') }
+ end
+ end
+ end
+ end
+
+ def highlight_timeout_total(source)
+ Gitlab::Metrics
+ .counter(:highlight_timeout, 'Counts the times highlights have timed out')
+ .get(source: source)
end
end
diff --git a/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb b/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb
new file mode 100644
index 00000000000..c50696f0883
--- /dev/null
+++ b/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::Menus::ConfluenceMenu do
+ let_it_be_with_refind(:project) { create(:project, has_external_wiki: true) }
+
+ let(:user) { project.owner }
+ let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
+
+ subject { described_class.new(context) }
+
+ describe 'render?' do
+ context 'when Confluence integration is not present' do
+ it 'returns false' do
+ expect(subject.render?).to eq false
+ end
+ end
+
+ context 'when Confluence integration is present' do
+ let!(:confluence) { create(:confluence_service, project: project, active: active) }
+
+ context 'when integration is disabled' do
+ let(:active) { false }
+
+ it 'returns false' do
+ expect(subject.render?).to eq false
+ end
+ end
+
+ context 'when issues integration is enabled' do
+ let(:active) { true }
+
+ it 'returns true' do
+ expect(subject.render?).to eq true
+ end
+
+ it 'does not contain any sub menu' do
+ expect(subject.items).to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb b/spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb
new file mode 100644
index 00000000000..b12f31017be
--- /dev/null
+++ b/spec/lib/sidebars/projects/menus/external_wiki_menu_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::Menus::ExternalWikiMenu do
+ let(:project) { build(:project) }
+ let(:user) { project.owner }
+ let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
+
+ subject { described_class.new(context) }
+
+ it 'does not contain any sub menu' do
+ expect(subject.items).to be_empty
+ end
+
+ describe '#render?' do
+ before do
+ expect(subject).to receive(:external_wiki).and_return(external_wiki).at_least(1)
+ end
+
+ context 'when active external issue tracker' do
+ let(:external_wiki) { build(:external_wiki_service, project: project) }
+
+ context 'is present' do
+ it 'returns true' do
+ expect(subject.render?).to be_truthy
+ end
+ end
+
+ context 'is not present' do
+ let(:external_wiki) { nil }
+
+ it 'returns false' do
+ expect(subject.render?).to be_falsey
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb b/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb
new file mode 100644
index 00000000000..21336d70ad2
--- /dev/null
+++ b/spec/lib/sidebars/projects/menus/wiki_menu_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::Menus::WikiMenu do
+ let(:project) { build(:project) }
+ let(:user) { project.owner }
+ let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
+
+ subject { described_class.new(context) }
+
+ it 'does not contain any sub menu' do
+ expect(subject.items).to be_empty
+ end
+
+ describe '#render?' do
+ context 'when user can access project wiki' do
+ it 'returns true' do
+ expect(subject.render?).to be true
+ end
+
+ context 'when user cannot access project wiki' do
+ let(:user) { nil }
+
+ it 'returns false' do
+ expect(subject.render?).to be false
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/projects/panel_spec.rb b/spec/lib/sidebars/projects/panel_spec.rb
index f4609682e6d..51d37bf69ea 100644
--- a/spec/lib/sidebars/projects/panel_spec.rb
+++ b/spec/lib/sidebars/projects/panel_spec.rb
@@ -11,4 +11,32 @@ RSpec.describe Sidebars::Projects::Panel do
it 'has a scope menu' do
expect(subject.scope_menu).to be_a(Sidebars::Projects::Menus::ScopeMenu)
end
+
+ context 'Confluence menu item' do
+ subject { described_class.new(context).instance_variable_get(:@menus) }
+
+ context 'when integration is present and active' do
+ let_it_be(:confluence) { create(:confluence_service, active: true) }
+
+ let(:project) { confluence.project }
+
+ it 'contains Confluence menu item' do
+ expect(subject.index { |i| i.is_a?(Sidebars::Projects::Menus::ConfluenceMenu) }).not_to be_nil
+ end
+
+ it 'does not contain Wiki menu item' do
+ expect(subject.index { |i| i.is_a?(Sidebars::Projects::Menus::WikiMenu) }).to be_nil
+ end
+ end
+
+ context 'when integration is not present' do
+ it 'does not contain Confluence menu item' do
+ expect(subject.index { |i| i.is_a?(Sidebars::Projects::Menus::ConfluenceMenu) }).to be_nil
+ end
+
+ it 'contains Wiki menu item' do
+ expect(subject.index { |i| i.is_a?(Sidebars::Projects::Menus::WikiMenu) }).not_to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/has_timelogs_report_spec.rb b/spec/models/concerns/has_timelogs_report_spec.rb
index f694fc350ee..f0dca47fae1 100644
--- a/spec/models/concerns/has_timelogs_report_spec.rb
+++ b/spec/models/concerns/has_timelogs_report_spec.rb
@@ -3,16 +3,20 @@
require 'spec_helper'
RSpec.describe HasTimelogsReport do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+
let(:group) { create(:group) }
- let(:issue) { create(:issue, project: create(:project, :public, group: group)) }
+ let(:project) { create(:project, :public, group: group) }
+ let(:issue1) { create(:issue, project: project) }
+ let(:merge_request1) { create(:merge_request, source_project: project) }
describe '#timelogs' do
- let!(:timelog1) { create_timelog(15.days.ago) }
- let!(:timelog2) { create_timelog(10.days.ago) }
- let!(:timelog3) { create_timelog(5.days.ago) }
- let(:start_time) { 20.days.ago }
- let(:end_time) { 8.days.ago }
+ let_it_be(:start_time) { 20.days.ago }
+ let_it_be(:end_time) { 8.days.ago }
+
+ let!(:timelog1) { create_timelog(15.days.ago, issue: issue1) }
+ let!(:timelog2) { create_timelog(10.days.ago, merge_request: merge_request1) }
+ let!(:timelog3) { create_timelog(5.days.ago, issue: issue1) }
before do
group.add_developer(user)
@@ -45,7 +49,7 @@ RSpec.describe HasTimelogsReport do
end
end
- def create_timelog(time)
- create(:timelog, issue: issue, user: user, spent_at: time)
+ def create_timelog(time, issue: nil, merge_request: nil)
+ create(:timelog, issue: issue, merge_request: merge_request, user: user, spent_at: time)
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 4b46c98117f..b534c761d2a 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2255,7 +2255,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
describe '#find_codequality_mr_diff_reports' do
let(:project) { create(:project, :repository) }
- let(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports, source_project: project) }
+ let(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports, source_project: project, id: 123456789) }
let(:pipeline) { merge_request.head_pipeline }
subject(:mr_diff_report) { merge_request.find_codequality_mr_diff_reports }
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index 31eeb36d1ee..c3432907112 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe Timelog do
subject { create(:timelog) }
- let(:issue) { create(:issue) }
- let(:merge_request) { create(:merge_request) }
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:merge_request) { create(:merge_request) }
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:issue).touch(true) }
@@ -54,27 +54,34 @@ RSpec.describe Timelog do
end
describe 'scopes' do
- describe 'for_issues_in_group' do
- it 'return timelogs created for group issues' do
- group = create(:group)
- subgroup = create(:group, parent: group)
+ let_it_be(:group) { create(:group) }
+ let_it_be(:group_project) { create(:project, :empty_repo, group: group) }
+ let_it_be(:group_issue) { create(:issue, project: group_project) }
+ let_it_be(:group_merge_request) { create(:merge_request, source_project: group_project) }
- create(:issue_timelog)
- timelog1 = create(:issue_timelog, issue: create(:issue, project: create(:project, group: group)))
- timelog2 = create(:issue_timelog, issue: create(:issue, project: create(:project, group: subgroup)))
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:subgroup_project) { create(:project, :empty_repo, group: subgroup) }
+ let_it_be(:subgroup_issue) { create(:issue, project: subgroup_project) }
+ let_it_be(:subgroup_merge_request) { create(:merge_request, source_project: subgroup_project) }
- expect(described_class.for_issues_in_group(group)).to contain_exactly(timelog1, timelog2)
+ let_it_be(:timelog) { create(:issue_timelog, spent_at: 65.days.ago) }
+ let_it_be(:timelog1) { create(:issue_timelog, spent_at: 15.days.ago, issue: group_issue) }
+ let_it_be(:timelog2) { create(:issue_timelog, spent_at: 5.days.ago, issue: subgroup_issue) }
+ let_it_be(:timelog3) { create(:merge_request_timelog, spent_at: 65.days.ago) }
+ let_it_be(:timelog4) { create(:merge_request_timelog, spent_at: 15.days.ago, merge_request: group_merge_request) }
+ let_it_be(:timelog5) { create(:merge_request_timelog, spent_at: 5.days.ago, merge_request: subgroup_merge_request) }
+
+ describe 'in_group' do
+ it 'return timelogs created for group issues and merge requests' do
+ expect(described_class.in_group(group)).to contain_exactly(timelog1, timelog2, timelog4, timelog5)
end
end
describe 'between_times' do
it 'returns collection of timelogs within given times' do
- create(:issue_timelog, spent_at: 65.days.ago)
- timelog1 = create(:issue_timelog, spent_at: 15.days.ago)
- timelog2 = create(:issue_timelog, spent_at: 5.days.ago)
- timelogs = described_class.between_times(20.days.ago, 1.day.ago)
+ timelogs = described_class.between_times(20.days.ago, 10.days.ago)
- expect(timelogs).to contain_exactly(timelog1, timelog2)
+ expect(timelogs).to contain_exactly(timelog1, timelog4)
end
end
end
diff --git a/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb b/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb
index 06d5422eed3..94a743d4d89 100644
--- a/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter_spec.rb
@@ -4,11 +4,12 @@ require 'spec_helper'
RSpec.describe Ci::PipelineArtifacts::CodeQualityMrDiffPresenter do
let(:pipeline_artifact) { create(:ci_pipeline_artifact, :with_codequality_mr_diff_report) }
+ let(:merge_request) { double(id: 123456789, new_paths: filenames) }
subject(:presenter) { described_class.new(pipeline_artifact) }
describe '#for_files' do
- subject(:quality_data) { presenter.for_files(filenames) }
+ subject(:quality_data) { presenter.for_files(merge_request) }
context 'when code quality has data' do
context 'when filenames is empty' do
diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb
index 09e5a76b46f..f207636283f 100644
--- a/spec/requests/api/graphql/ci/pipelines_spec.rb
+++ b/spec/requests/api/graphql/ci/pipelines_spec.rb
@@ -51,6 +51,87 @@ RSpec.describe 'Query.project(fullPath).pipelines' do
end
end
+ describe '.stages' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ let_it_be(:stage) { create(:ci_stage_entity, pipeline: pipeline, project: project) }
+ let_it_be(:other_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'other') }
+
+ let(:first_n) { var('Int') }
+ let(:query_path) do
+ [
+ [:project, { full_path: project.full_path }],
+ [:pipelines],
+ [:nodes],
+ [:stages, { first: first_n }],
+ [:nodes]
+ ]
+ end
+
+ let(:query) do
+ with_signature([first_n], wrap_fields(query_graphql_path(query_path, :name)))
+ end
+
+ before_all do
+ # see app/services/ci/ensure_stage_service.rb to explain why we use stage_id
+ create(:ci_build, pipeline: pipeline, stage_id: stage.id, name: 'linux: [foo]')
+ create(:ci_build, pipeline: pipeline, stage_id: stage.id, name: 'linux: [bar]')
+ create(:ci_build, pipeline: pipeline, stage_id: other_stage.id, name: 'linux: [baz]')
+ end
+
+ it 'is null if the user is a guest' do
+ project.add_guest(user)
+
+ post_graphql(query, current_user: user, variables: first_n.with(1))
+
+ expect(graphql_data_at(:project, :pipelines, :nodes)).to contain_exactly a_hash_including('stages' => be_nil)
+ end
+
+ it 'is present if the user has reporter access' do
+ project.add_reporter(user)
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :pipelines, :nodes, :stages, :nodes, :name))
+ .to contain_exactly(eq(stage.name), eq(other_stage.name))
+ end
+
+ describe '.groups' do
+ let(:query_path) do
+ [
+ [:project, { full_path: project.full_path }],
+ [:pipelines],
+ [:nodes],
+ [:stages],
+ [:nodes],
+ [:groups],
+ [:nodes]
+ ]
+ end
+
+ let(:query) do
+ wrap_fields(query_graphql_path(query_path, :name))
+ end
+
+ it 'is empty if the user is a guest' do
+ project.add_guest(user)
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :pipelines, :nodes, :stages, :nodes, :groups)).to be_empty
+ end
+
+ it 'is present if the user has reporter access' do
+ project.add_reporter(user)
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :pipelines, :nodes, :stages, :nodes, :groups, :nodes, :name))
+ .to contain_exactly('linux', 'linux')
+ end
+ end
+ end
+
describe '.jobs' do
let(:first_n) { var('Int') }
let(:query_path) do
diff --git a/spec/serializers/ci/codequality_mr_diff_entity_spec.rb b/spec/serializers/ci/codequality_mr_diff_entity_spec.rb
index 82708908d95..4f161c36b06 100644
--- a/spec/serializers/ci/codequality_mr_diff_entity_spec.rb
+++ b/spec/serializers/ci/codequality_mr_diff_entity_spec.rb
@@ -4,18 +4,18 @@ require 'spec_helper'
RSpec.describe Ci::CodequalityMrDiffEntity do
let(:entity) { described_class.new(mr_diff_report) }
- let(:mr_diff_report) { Gitlab::Ci::Reports::CodequalityMrDiff.new(codequality_report) }
+ let(:mr_diff_report) { Gitlab::Ci::Reports::CodequalityMrDiff.new(codequality_report.all_degradations) }
let(:codequality_report) { Gitlab::Ci::Reports::CodequalityReports.new }
- let(:degradation_1) { build(:codequality_degradation_1) }
- let(:degradation_2) { build(:codequality_degradation_2) }
+ let(:major) { build(:codequality_degradation, :major) }
+ let(:minor) { build(:codequality_degradation, :minor) }
describe '#as_json' do
subject(:report) { entity.as_json }
context 'when quality report has degradations' do
before do
- codequality_report.add_degradation(degradation_1)
- codequality_report.add_degradation(degradation_2)
+ codequality_report.add_degradation(major)
+ codequality_report.add_degradation(minor)
end
it 'contains correct codequality mr diff report', :aggregate_failures do
diff --git a/spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb b/spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb
index 906ca36041f..6afbc3b8353 100644
--- a/spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb
+++ b/spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb
@@ -4,18 +4,18 @@ require 'spec_helper'
RSpec.describe Ci::CodequalityMrDiffReportSerializer do
let(:serializer) { described_class.new.represent(mr_diff_report) }
- let(:mr_diff_report) { Gitlab::Ci::Reports::CodequalityMrDiff.new(codequality_report) }
+ let(:mr_diff_report) { Gitlab::Ci::Reports::CodequalityMrDiff.new(codequality_report.all_degradations) }
let(:codequality_report) { Gitlab::Ci::Reports::CodequalityReports.new }
- let(:degradation_1) { build(:codequality_degradation_1) }
- let(:degradation_2) { build(:codequality_degradation_2) }
+ let(:major) { build(:codequality_degradation, :major) }
+ let(:minor) { build(:codequality_degradation, :minor) }
describe '#to_json' do
subject { serializer.as_json }
context 'when quality report has degradations' do
before do
- codequality_report.add_degradation(degradation_1)
- codequality_report.add_degradation(degradation_2)
+ codequality_report.add_degradation(major)
+ codequality_report.add_degradation(minor)
end
it 'matches the schema' do
diff --git a/spec/serializers/group_issuable_autocomplete_entity_spec.rb b/spec/serializers/group_issuable_autocomplete_entity_spec.rb
new file mode 100644
index 00000000000..86ef9dea23b
--- /dev/null
+++ b/spec/serializers/group_issuable_autocomplete_entity_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GroupIssuableAutocompleteEntity do
+ let(:group) { build_stubbed(:group) }
+ let(:project) { build_stubbed(:project, group: group) }
+ let(:issue) { build_stubbed(:issue, project: project) }
+
+ describe '#represent' do
+ subject { described_class.new(issue, parent_group: group).as_json }
+
+ it 'includes the iid, title, and reference' do
+ expect(subject).to include(:iid, :title, :reference)
+ end
+ end
+end
diff --git a/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb b/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb
index 5d747a09f2a..63bc7a1caf8 100644
--- a/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb
+++ b/spec/services/ci/generate_codequality_mr_diff_report_service_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Ci::GenerateCodequalityMrDiffReportService do
subject { service.execute(base_pipeline, head_pipeline) }
context 'when head pipeline has codequality mr diff report' do
- let!(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports, source_project: project) }
+ let!(:merge_request) { create(:merge_request, :with_codequality_mr_diff_reports, source_project: project, id: 123456789) }
let!(:service) { described_class.new(project, nil, id: merge_request.id) }
let!(:head_pipeline) { merge_request.head_pipeline }
let!(:base_pipeline) { nil }
@@ -18,7 +18,7 @@ RSpec.describe Ci::GenerateCodequalityMrDiffReportService do
it 'returns status and data', :aggregate_failures do
expect_any_instance_of(Ci::PipelineArtifact) do |instance|
expect(instance).to receive(:present)
- expect(instance).to receive(:for_files).with(merge_request.new_paths).and_call_original
+ expect(instance).to receive(:for_files).with(merge_request).and_call_original
end
expect(subject[:status]).to eq(:parsed)
diff --git a/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb b/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb
index 0c48f15d726..5568052e346 100644
--- a/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb
+++ b/spec/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service_spec.rb
@@ -4,58 +4,76 @@ require 'spec_helper'
RSpec.describe ::Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService do
describe '#execute' do
- subject(:pipeline_artifact) { described_class.new.execute(pipeline) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+ let(:head_pipeline) { create(:ci_pipeline, :success, :with_codequality_reports, project: project, merge_requests_as_head_pipeline: [merge_request]) }
+ let(:base_pipeline) { create(:ci_pipeline, :success, project: project, ref: merge_request.target_branch, sha: merge_request.diff_base_sha) }
- context 'when pipeline has codequality reports' do
- let(:project) { create(:project, :repository) }
+ subject { described_class.new(head_pipeline).execute }
- describe 'pipeline completed status' do
- using RSpec::Parameterized::TableSyntax
+ context 'when there are codequality reports' do
+ context 'when pipeline passes' do
+ context 'when degradations are present' do
+ context 'when degradations already present in target branch pipeline' do
+ before do
+ create(:ci_build, :success, :codequality_reports, name: 'codequality', pipeline: base_pipeline, project: project)
+ end
- where(:status, :result) do
- :success | 1
- :failed | 1
- :canceled | 1
- :skipped | 1
- end
-
- with_them do
- let(:pipeline) { create(:ci_pipeline, :with_codequality_reports, status: status, project: project) }
-
- it 'creates a pipeline artifact' do
- expect { pipeline_artifact }.to change(Ci::PipelineArtifact, :count).by(result)
+ it "does not persist a pipeline artifact" do
+ expect { subject }.not_to change { Ci::PipelineArtifact.count }
+ end
end
- it 'persists the default file name' do
- expect(pipeline_artifact.file.filename).to eq('code_quality_mr_diff.json')
- end
+ context 'when degradation is not present in target branch pipeline' do
+ before do
+ create(:ci_build, :success, :codequality_reports_without_degradation, name: 'codequality', pipeline: base_pipeline, project: project)
+ end
- it 'sets expire_at to 1 week' do
- freeze_time do
- expect(pipeline_artifact.expire_at).to eq(1.week.from_now)
+ it 'persists a pipeline artifact' do
+ expect { subject }.to change { Ci::PipelineArtifact.count }.by(1)
+ end
+
+ it 'persists the default file name' do
+ subject
+
+ pipeline_artifact = Ci::PipelineArtifact.first
+
+ expect(pipeline_artifact.file.filename).to eq('code_quality_mr_diff.json')
+ end
+
+ it 'sets expire_at to 1 week' do
+ freeze_time do
+ subject
+
+ pipeline_artifact = Ci::PipelineArtifact.first
+
+ expect(pipeline_artifact.expire_at).to eq(1.week.from_now)
+ end
+ end
+
+ it 'does not persist the same artifact twice' do
+ 2.times { described_class.new(head_pipeline).execute }
+
+ expect { subject }.not_to change { Ci::PipelineArtifact.count }
end
end
end
end
+ end
- context 'when pipeline artifact has already been created' do
- let(:pipeline) { create(:ci_pipeline, :with_codequality_reports, project: project) }
+ context 'when there are no codequality reports for head pipeline' do
+ let(:head_pipeline) { create(:ci_pipeline, :success, project: project, merge_requests_as_head_pipeline: [merge_request]) }
- it 'does not persist the same artifact twice' do
- 2.times { described_class.new.execute(pipeline) }
-
- expect(Ci::PipelineArtifact.count).to eq(1)
- end
+ it "does not persist a pipeline artifact" do
+ expect { subject }.not_to change { Ci::PipelineArtifact.count }
end
end
- context 'when pipeline is not completed and codequality report does not exist' do
- let(:pipeline) { create(:ci_pipeline, :running) }
+ context 'when there are no codequality reports for base pipeline' do
+ let(:head_pipeline) { create(:ci_pipeline, :success, project: project, merge_requests_as_head_pipeline: [merge_request]) }
- it 'does not persist data' do
- pipeline_artifact
-
- expect(Ci::PipelineArtifact.count).to eq(0)
+ it "does not persist a pipeline artifact" do
+ expect { subject }.not_to change { Ci::PipelineArtifact.count }
end
end
end
diff --git a/spec/services/groups/autocomplete_service_spec.rb b/spec/services/groups/autocomplete_service_spec.rb
new file mode 100644
index 00000000000..00d0ad3b347
--- /dev/null
+++ b/spec/services/groups/autocomplete_service_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::AutocompleteService do
+ let_it_be(:group, refind: true) { create(:group, :nested, :private, avatar: fixture_file_upload('spec/fixtures/dk.png')) }
+ let_it_be(:sub_group) { create(:group, :private, parent: group) }
+
+ let(:user) { create(:user) }
+
+ subject { described_class.new(group, user) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ def expect_labels_to_equal(labels, expected_labels)
+ extract_title = lambda { |label| label['title'] }
+ expect(labels.map(&extract_title)).to match_array(expected_labels.map(&extract_title))
+ end
+
+ describe '#labels_as_hash' do
+ let!(:label1) { create(:group_label, group: group) }
+ let!(:label2) { create(:group_label, group: group) }
+ let!(:sub_group_label) { create(:group_label, group: sub_group) }
+ let!(:parent_group_label) { create(:group_label, group: group.parent) }
+
+ it 'returns labels from own group and ancestor groups' do
+ results = subject.labels_as_hash(nil)
+
+ expected_labels = [label1, label2, parent_group_label]
+
+ expect_labels_to_equal(results, expected_labels)
+ end
+ end
+
+ describe '#issues' do
+ let(:project) { create(:project, group: group) }
+ let(:sub_group_project) { create(:project, group: sub_group) }
+
+ let!(:project_issue) { create(:issue, project: project) }
+ let!(:sub_group_project_issue) { create(:issue, confidential: true, project: sub_group_project) }
+
+ it 'returns issues in group and subgroups' do
+ issues = subject.issues
+
+ expect(issues.map(&:iid)).to contain_exactly(project_issue.iid, sub_group_project_issue.iid)
+ expect(issues.map(&:title)).to contain_exactly(project_issue.title, sub_group_project_issue.title)
+ end
+
+ it 'returns only confidential issues if confidential_only is true' do
+ issues = subject.issues(confidential_only: true)
+
+ expect(issues.map(&:iid)).to contain_exactly(sub_group_project_issue.iid)
+ expect(issues.map(&:title)).to contain_exactly(sub_group_project_issue.title)
+ end
+ end
+
+ describe '#merge_requests' do
+ let(:project) { create(:project, :repository, group: group) }
+ let(:sub_group_project) { create(:project, :repository, group: sub_group) }
+
+ let!(:project_mr) { create(:merge_request, source_project: project) }
+ let!(:sub_group_project_mr) { create(:merge_request, source_project: sub_group_project) }
+
+ it 'returns merge requests in group and subgroups' do
+ expect(subject.merge_requests.map(&:iid)).to contain_exactly(project_mr.iid, sub_group_project_mr.iid)
+ expect(subject.merge_requests.map(&:title)).to contain_exactly(project_mr.title, sub_group_project_mr.title)
+ end
+ end
+
+ describe '#milestones' do
+ let!(:group_milestone) { create(:milestone, group: group) }
+ let!(:subgroup_milestone) { create(:milestone, group: sub_group) }
+
+ before do
+ sub_group.add_maintainer(user)
+ end
+
+ context 'when group is public' do
+ let(:public_group) { create(:group, :public) }
+ let(:public_subgroup) { create(:group, :public, parent: public_group) }
+
+ before do
+ group_milestone.update!(group: public_group)
+ subgroup_milestone.update!(group: public_subgroup)
+ end
+
+ it 'returns milestones from groups and subgroups' do
+ subject = described_class.new(public_subgroup, user)
+
+ expect(subject.milestones.map(&:iid)).to contain_exactly(group_milestone.iid, subgroup_milestone.iid)
+ expect(subject.milestones.map(&:title)).to contain_exactly(group_milestone.title, subgroup_milestone.title)
+ end
+ end
+
+ it 'returns milestones from group' do
+ expect(subject.milestones.map(&:iid)).to contain_exactly(group_milestone.iid)
+ expect(subject.milestones.map(&:title)).to contain_exactly(group_milestone.title)
+ end
+
+ it 'returns milestones from groups and subgroups' do
+ milestones = described_class.new(sub_group, user).milestones
+
+ expect(milestones.map(&:iid)).to contain_exactly(group_milestone.iid, subgroup_milestone.iid)
+ expect(milestones.map(&:title)).to contain_exactly(group_milestone.title, subgroup_milestone.title)
+ end
+
+ it 'returns only milestones that user can read' do
+ user = create(:user)
+ sub_group.add_guest(user)
+
+ milestones = described_class.new(sub_group, user).milestones
+
+ expect(milestones.map(&:iid)).to contain_exactly(subgroup_milestone.iid)
+ expect(milestones.map(&:title)).to contain_exactly(subgroup_milestone.title)
+ end
+ end
+end
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index 7bb9570e90a..8654f10f72c 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -783,13 +783,39 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
- describe 'wiki entry tab' do
- let(:can_read_wiki) { true }
+ describe 'Confluence' do
+ let!(:service) { create(:confluence_service, project: project, active: active) }
before do
- allow(view).to receive(:can?).with(user, :read_wiki, project).and_return(can_read_wiki)
+ render
end
+ context 'when the Confluence integration is active' do
+ let(:active) { true }
+
+ it 'shows the Confluence link' do
+ expect(rendered).to have_link('Confluence', href: project_wikis_confluence_path(project))
+ end
+
+ it 'does not show the GitLab wiki link' do
+ expect(rendered).not_to have_link('Wiki')
+ end
+ end
+
+ context 'when it is disabled' do
+ let(:active) { false }
+
+ it 'does not show the Confluence link' do
+ expect(rendered).not_to have_link('Confluence')
+ end
+
+ it 'shows the GitLab wiki link' do
+ expect(rendered).to have_link('Wiki', href: wiki_path(project.wiki))
+ end
+ end
+ end
+
+ describe 'Wiki' do
describe 'when wiki is enabled' do
it 'shows the wiki tab with the wiki internal link' do
render
@@ -799,9 +825,9 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
describe 'when wiki is disabled' do
- let(:can_read_wiki) { false }
+ let(:user) { nil }
- it 'does not show the wiki tab' do
+ it 'does not show the wiki link' do
render
expect(rendered).not_to have_link('Wiki')
@@ -809,7 +835,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
- describe 'external wiki entry tab' do
+ describe 'External Wiki' do
let(:properties) { { 'external_wiki_url' => 'https://gitlab.com' } }
let(:service_status) { true }
@@ -829,7 +855,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
context 'when it is disabled' do
let(:service_status) { false }
- it 'does not show the external wiki tab' do
+ it 'does not show the external wiki link' do
render
expect(rendered).not_to have_link('External wiki')
@@ -837,38 +863,6 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
- describe 'confluence tab' do
- let!(:service) { create(:confluence_service, project: project, active: active) }
-
- before do
- render
- end
-
- context 'when the Confluence integration is active' do
- let(:active) { true }
-
- it 'shows the Confluence tab' do
- expect(rendered).to have_link('Confluence', href: project_wikis_confluence_path(project))
- end
-
- it 'does not show the GitLab wiki tab' do
- expect(rendered).not_to have_link('Wiki')
- end
- end
-
- context 'when it is disabled' do
- let(:active) { false }
-
- it 'does not show the Confluence tab' do
- expect(rendered).not_to have_link('Confluence')
- end
-
- it 'shows the GitLab wiki tab' do
- expect(rendered).to have_link('Wiki', href: wiki_path(project.wiki))
- end
- end
- end
-
describe 'operations settings tab' do
describe 'archive projects' do
before do
diff --git a/spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb b/spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb
index be351032b58..5096691270a 100644
--- a/spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb
+++ b/spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb
@@ -21,8 +21,8 @@ RSpec.describe ::Ci::PipelineArtifacts::CreateQualityReportWorker do
it_behaves_like 'an idempotent worker' do
let(:job_args) { pipeline_id }
- it 'creates a pipeline artifact' do
- expect { subject }.to change { pipeline.pipeline_artifacts.count }.by(1)
+ it 'does not create another pipeline artifact if already has one' do
+ expect { subject }.not_to change { pipeline.pipeline_artifacts.count }
end
end
end
diff --git a/workhorse/internal/upstream/routes.go b/workhorse/internal/upstream/routes.go
index a2a5f93613b..230b67ed059 100644
--- a/workhorse/internal/upstream/routes.go
+++ b/workhorse/internal/upstream/routes.go
@@ -286,6 +286,9 @@ func configureRoutes(u *upstream) {
// Terraform Module Package Repository
u.route("PUT", apiProjectPattern+`packages/terraform/modules/`, upload.BodyUploader(api, signingProxy, preparers.packages)),
+ // Helm Artifact Repository
+ u.route("POST", apiProjectPattern+`packages/helm/api/[^/]+/charts\z`, upload.Accelerate(api, signingProxy, preparers.packages)),
+
// We are porting API to disk acceleration
// we need to declare each routes until we have fixed all the routes on the rails codebase.
// Overall status can be seen at https://gitlab.com/groups/gitlab-org/-/epics/1802#current-status
diff --git a/workhorse/upload_test.go b/workhorse/upload_test.go
index a44bf1a4aeb..cef0f3fe052 100644
--- a/workhorse/upload_test.go
+++ b/workhorse/upload_test.go
@@ -142,6 +142,9 @@ func TestAcceleratedUpload(t *testing.T) {
{"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/issues/30/metric_images`, true},
{"POST", `/my/project/-/requirements_management/requirements/import_csv`, true},
{"POST", `/my/project/-/requirements_management/requirements/import_csv/`, true},
+ {"POST", "/api/v4/projects/2412/packages/helm/api/stable/charts", true},
+ {"POST", "/api/v4/projects/group%2Fproject/packages/helm/api/stable/charts", true},
+ {"POST", "/api/v4/projects/group%2Fsubgroup%2Fproject/packages/helm/api/stable/charts", true},
}
for _, tt := range tests {
@@ -232,6 +235,8 @@ func TestUnacceleratedUploads(t *testing.T) {
{"POST", `/api/v4/projects/group/subgroup/project/packages/pypi`},
{"POST", `/api/v4/projects/group/project/packages/pypi`},
{"POST", `/api/v4/projects/group/subgroup/project/packages/pypi`},
+ {"POST", "/api/v4/projects/group/project/packages/helm/api/stable/charts"},
+ {"POST", "/api/v4/projects/group/subgroup%2Fproject/packages/helm/api/stable/charts"},
{"POST", `/api/v4/projects/group/project/issues/30/metric_images`},
{"POST", `/api/v4/projects/group/subgroup/project/issues/30/metric_images`},
}