diff options
60 files changed, 1165 insertions, 439 deletions
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index 445602a8765..470c679b8ba 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -3,7 +3,6 @@ import Dropzone from 'dropzone'; import $ from 'jquery'; import { sprintf, __ } from '~/locale'; -import { trackUploadFileFormSubmitted } from '~/projects/upload_file_experiment'; import { HIDDEN_CLASS } from '../lib/utils/constants'; import csrf from '../lib/utils/csrf'; import { visitUrl } from '../lib/utils/url_utility'; @@ -85,8 +84,6 @@ export default class BlobFileDropzone { e.preventDefault(); e.stopPropagation(); - trackUploadFileFormSubmitted(); - if (dropzone[0].dropzone.getQueuedFiles().length === 0) { // eslint-disable-next-line no-alert alert(__('Please select a file')); diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index 6d9b56b4bb8..173c82ef9b0 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -4,7 +4,6 @@ import $ from 'jquery'; import initPopover from '~/blob/suggest_gitlab_ci_yml'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils'; -import { initUploadFileTrigger } from '~/projects/upload_file_experiment'; import Tracking from '~/tracking'; import BlobFileDropzone from '../blob/blob_file_dropzone'; import NewCommitForm from '../new_commit_form'; @@ -48,7 +47,6 @@ export const initUploadForm = () => { new NewCommitForm(uploadBlobForm); disableButtonIfEmptyField(uploadBlobForm.find('.js-commit-message'), '.btn-upload-file'); - initUploadFileTrigger(); } }; diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js index 5649c47d7e8..0a4311ec73a 100644 --- a/app/assets/javascripts/pages/admin/abuse_reports/index.js +++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js @@ -1,8 +1,5 @@ -/* eslint-disable no-new */ import UsersSelect from '~/users_select'; import AbuseReports from './abuse_reports'; -document.addEventListener('DOMContentLoaded', () => { - new AbuseReports(); - new UsersSelect(); -}); +new AbuseReports(); /* eslint-disable-line no-new */ +new UsersSelect(); /* eslint-disable-line no-new */ diff --git a/app/assets/javascripts/pages/admin/application_settings/general/index.js b/app/assets/javascripts/pages/admin/application_settings/general/index.js index e2a6f4208b9..eda1a9d3599 100644 --- a/app/assets/javascripts/pages/admin/application_settings/general/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/general/index.js @@ -1,7 +1,3 @@ -// This is a true violation of @gitlab/no-runtime-template-compiler, as it -// relies on app/views/admin/application_settings/_gitpod.html.haml for its -// template. -/* eslint-disable @gitlab/no-runtime-template-compiler */ import Vue from 'vue'; import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue'; import initUserInternalRegexPlaceholder from '../account_and_limits'; @@ -9,17 +5,23 @@ import initUserInternalRegexPlaceholder from '../account_and_limits'; (() => { initUserInternalRegexPlaceholder(); - const gitpodSettingEl = document.querySelector('#js-gitpod-settings-help-text'); - if (!gitpodSettingEl) { + const el = document.querySelector('#js-gitpod-settings-help-text'); + if (!el) { return; } + const { message, messageUrl } = el.dataset; + // eslint-disable-next-line no-new new Vue({ - el: gitpodSettingEl, - name: 'GitpodSettings', - components: { - IntegrationHelpText, + el, + render(createElement) { + return createElement(IntegrationHelpText, { + props: { + message, + messageUrl, + }, + }); }, }); })(); diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index a0831c7df41..83e43d7ac48 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -5,6 +5,7 @@ import BlobViewer from '~/blob/viewer/index'; import { initUploadForm } from '~/blob_edit/blob_bundle'; import leaveByUrl from '~/namespaces/leave_by_url'; import initVueNotificationsDropdown from '~/notifications'; +import { initUploadFileTrigger } from '~/projects/upload_file_experiment'; import initReadMore from '~/read_more'; import UserCallout from '~/user_callout'; import Star from '../../../star'; @@ -41,3 +42,5 @@ leaveByUrl('project'); initVueNotificationsDropdown(); new ShortcutsNavigation(); // eslint-disable-line no-new + +initUploadFileTrigger(); diff --git a/app/assets/javascripts/projects/details/upload_button.vue b/app/assets/javascripts/projects/details/upload_button.vue index a89ea34c438..5b19f15c233 100644 --- a/app/assets/javascripts/projects/details/upload_button.vue +++ b/app/assets/javascripts/projects/details/upload_button.vue @@ -1,6 +1,7 @@ <script> import { GlButton, GlModalDirective } from '@gitlab/ui'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; +import { trackFileUploadEvent } from '../upload_file_experiment_tracking'; const UPLOAD_BLOB_MODAL_ID = 'details-modal-upload-blob'; @@ -16,7 +17,7 @@ export default { targetBranch: { default: '', }, - origionalBranch: { + originalBranch: { default: '', }, canPushCode: { @@ -29,19 +30,28 @@ export default { default: '', }, }, + methods: { + trackOpenModal() { + trackFileUploadEvent('click_upload_modal_trigger'); + }, + }, uploadBlobModalId: UPLOAD_BLOB_MODAL_ID, }; </script> <template> <span> - <gl-button v-gl-modal="$options.uploadBlobModalId" icon="upload">{{ - __('Upload File') - }}</gl-button> + <gl-button + v-gl-modal="$options.uploadBlobModalId" + icon="upload" + data-testid="upload-file-button" + @click="trackOpenModal" + >{{ __('Upload File') }}</gl-button + > <upload-blob-modal :modal-id="$options.uploadBlobModalId" :commit-message="__('Upload New File')" :target-branch="targetBranch" - :origional-branch="origionalBranch" + :original-branch="originalBranch" :can-push-code="canPushCode" :path="path" /> diff --git a/app/assets/javascripts/projects/upload_file_experiment.js b/app/assets/javascripts/projects/upload_file_experiment.js index 7d61df36a75..a7519f2bce8 100644 --- a/app/assets/javascripts/projects/upload_file_experiment.js +++ b/app/assets/javascripts/projects/upload_file_experiment.js @@ -1,24 +1,33 @@ -import ExperimentTracking from '~/experimentation/experiment_tracking'; +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import createRouter from '~/repository/router'; +import UploadButton from './details/upload_button.vue'; -function trackEvent(eventName) { - const isEmpty = Boolean(document.querySelector('.project-home-panel.empty-project')); - const property = isEmpty ? 'empty' : 'nonempty'; - const label = 'blob-upload-modal'; - const Tracking = new ExperimentTracking('empty_repo_upload', { label, property }); - - Tracking.event(eventName); -} - -export function initUploadFileTrigger() { +export const initUploadFileTrigger = () => { const uploadFileTriggerEl = document.querySelector('.js-upload-file-experiment-trigger'); - if (uploadFileTriggerEl) { - uploadFileTriggerEl.addEventListener('click', () => { - trackEvent('click_upload_modal_trigger'); - }); - } -} + if (!uploadFileTriggerEl) return false; + + const { + targetBranch, + originalBranch, + canPushCode, + path, + projectPath, + } = uploadFileTriggerEl.dataset; -export function trackUploadFileFormSubmitted() { - trackEvent('click_upload_modal_form_submit'); -} + return new Vue({ + el: uploadFileTriggerEl, + router: createRouter(projectPath, originalBranch), + provide: { + targetBranch, + originalBranch, + canPushCode: parseBoolean(canPushCode), + path, + projectPath, + }, + render(h) { + return h(UploadButton); + }, + }); +}; diff --git a/app/assets/javascripts/projects/upload_file_experiment_tracking.js b/app/assets/javascripts/projects/upload_file_experiment_tracking.js new file mode 100644 index 00000000000..c5e93f19b32 --- /dev/null +++ b/app/assets/javascripts/projects/upload_file_experiment_tracking.js @@ -0,0 +1,9 @@ +import ExperimentTracking from '~/experimentation/experiment_tracking'; + +export const trackFileUploadEvent = (eventName) => { + const isEmpty = Boolean(document.querySelector('.project-home-panel.empty-project')); + const property = isEmpty ? 'empty' : 'nonempty'; + const label = 'blob-upload-modal'; + const FileUploadTracking = new ExperimentTracking('empty_repo_upload', { label, property }); + FileUploadTracking.event(eventName); +}; diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue index 4cdfc5e947a..ec7ba469ca0 100644 --- a/app/assets/javascripts/repository/components/upload_blob_modal.vue +++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue @@ -15,6 +15,7 @@ import { ContentTypeMultipartFormData } from '~/lib/utils/headers'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { visitUrl, joinPaths } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; +import { trackFileUploadEvent } from '~/projects/upload_file_experiment_tracking'; import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; const PRIMARY_OPTIONS_TEXT = __('Upload file'); @@ -62,7 +63,7 @@ export default { type: String, required: true, }, - origionalBranch: { + originalBranch: { type: String, required: true, }, @@ -113,7 +114,7 @@ export default { return numberToHumanSize(this.file.size); }, showCreateNewMrToggle() { - return this.canPushCode && this.target !== this.origionalBranch; + return this.canPushCode && this.target !== this.originalBranch; }, formCompleted() { return this.file && this.commit && this.target; @@ -158,6 +159,7 @@ export default { }, }) .then((response) => { + trackFileUploadEvent('click_upload_modal_form_submit'); visitUrl(response.data.filePath); }) .catch(() => { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue deleted file mode 100644 index 3cd003461b3..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue +++ /dev/null @@ -1,49 +0,0 @@ -<script> -import { GlButton, GlModalDirective } from '@gitlab/ui'; -import { sprintf, s__ } from '~/locale'; - -export default { - name: 'MRWidgetMergeHelp', - components: { - GlButton, - }, - directives: { - GlModalDirective, - }, - props: { - missingBranch: { - type: String, - required: false, - default: '', - }, - }, - computed: { - missingBranchInfo() { - return sprintf( - s__( - 'mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the', - ), - { branch: this.missingBranch }, - ); - }, - }, -}; -</script> -<template> - <section class="gl-py-3 gl-pr-3 gl-pl-5 gl-ml-7 mr-widget-help gl-font-style-italic"> - <template v-if="missingBranch"> - {{ missingBranchInfo }} - </template> - <template v-else> - {{ s__('mrWidget|You can merge this merge request manually using the') }} - </template> - - <gl-button - v-gl-modal-directive="'modal-merge-info'" - variant="link" - class="gl-mt-n2 js-open-modal-help" - > - {{ s__('mrWidget|command line') }} - </gl-button> - </section> -</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index e2a0689dffc..89b095fbfc1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -18,7 +18,6 @@ import GroupedTestReportsApp from '../reports/grouped_test_report/grouped_test_r import Loading from './components/loading.vue'; import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue'; import WidgetHeader from './components/mr_widget_header.vue'; -import WidgetMergeHelp from './components/mr_widget_merge_help.vue'; import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue'; import WidgetRelatedLinks from './components/mr_widget_related_links.vue'; import WidgetSuggestPipeline from './components/mr_widget_suggest_pipeline.vue'; @@ -59,7 +58,6 @@ export default { // ExtensionsContainer, 'mr-widget-header': WidgetHeader, 'mr-widget-suggest-pipeline': WidgetSuggestPipeline, - 'mr-widget-merge-help': WidgetMergeHelp, MrWidgetPipelineContainer, 'mr-widget-related-links': WidgetRelatedLinks, MrWidgetAlertMessage, @@ -140,9 +138,6 @@ export default { componentName() { return stateMaps.stateToComponentMap[this.mr.state]; }, - shouldRenderMergeHelp() { - return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1; - }, hasPipelineMustSucceedConflict() { return !this.mr.hasCI && this.mr.onlyAllowMergeIfPipelineSucceeds; }, @@ -530,9 +525,6 @@ export default { <source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" /> </div> </div> - <div v-if="shouldRenderMergeHelp" class="mr-widget-footer"> - <mr-widget-merge-help /> - </div> </div> <mr-widget-pipeline-container v-if="shouldRenderMergedPipeline" diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb index 01dc0b1ab00..5a885349467 100644 --- a/app/controllers/dashboard/snippets_controller.rb +++ b/app/controllers/dashboard/snippets_controller.rb @@ -19,6 +19,7 @@ class Dashboard::SnippetsController < Dashboard::ApplicationController .page(params[:page]) .inc_author .inc_projects_namespace_route + .inc_statistics return if redirect_out_of_range(@snippets) diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb index 91ab18f2f55..617cc2e7f3d 100644 --- a/app/controllers/explore/snippets_controller.rb +++ b/app/controllers/explore/snippets_controller.rb @@ -11,6 +11,7 @@ class Explore::SnippetsController < Explore::ApplicationController .page(params[:page]) .without_count .inc_author + .inc_statistics @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet') end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 779e149bb9c..ff28c3be298 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -22,6 +22,7 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController .execute .page(params[:page]) .inc_author + .inc_statistics return if redirect_out_of_range(@snippets) diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 913b1e3bb6e..1c6168dbc2c 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -24,6 +24,7 @@ class SnippetsController < Snippets::ApplicationController .execute .page(params[:page]) .inc_author + .inc_statistics return if redirect_out_of_range(@snippets) diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 1be7e240c1a..36f4fced147 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -68,4 +68,18 @@ module SnippetsHelper title: 'Download', rel: 'noopener noreferrer') end + + def snippet_file_count(snippet) + file_count = snippet.statistics&.file_count + + return unless file_count&.nonzero? + + tooltip = n_('%d file', '%d files', file_count) % file_count + + tag.span(class: 'file_count', title: tooltip, data: { toggle: 'tooltip', container: 'body' }) do + concat(sprite_icon('documents', css_class: 'gl-vertical-align-middle')) + concat(' ') + concat(file_count) + end + end end diff --git a/app/models/group.rb b/app/models/group.rb index ba0d70b9c13..9f8a9996f31 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -340,6 +340,13 @@ class Group < Namespace has_owner?(user) && members_with_parents.owners.size == 1 end + def last_blocked_owner?(user) + return false if members_with_parents.owners.any? + + blocked_owners = members.blocked.where(access_level: Gitlab::Access::OWNER) + blocked_owners.size == 1 && blocked_owners.exists?(user_id: user) + end + def ldap_synced? false end diff --git a/app/models/member.rb b/app/models/member.rb index 62fe757683f..38574d67cb6 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -75,7 +75,20 @@ class Member < ApplicationRecord left_join_users .where(user_ok) - .where(requested_at: nil) + .non_request + .non_minimal_access + .reorder(nil) + end + + scope :blocked, -> do + is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil)) + user_is_blocked = User.arel_table[:state].eq(:blocked) + + user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_blocked) + + left_join_users + .where(user_ok) + .non_request .non_minimal_access .reorder(nil) end diff --git a/app/models/packages/maven/metadatum.rb b/app/models/packages/maven/metadatum.rb index b7f27fb9e06..7aed274216b 100644 --- a/app/models/packages/maven/metadatum.rb +++ b/app/models/packages/maven/metadatum.rb @@ -18,6 +18,13 @@ class Packages::Maven::Metadatum < ApplicationRecord validate :maven_package_type + scope :for_package_ids, -> (package_ids) { where(package_id: package_ids) } + scope :order_created, -> { reorder('created_at ASC') } + + def self.pluck_app_name + pluck(:app_name) + end + private def maven_package_type diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 8edf31bd661..5fdd4551982 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -82,6 +82,7 @@ class Snippet < ApplicationRecord scope :fresh, -> { order("created_at DESC") } scope :inc_author, -> { includes(:author) } scope :inc_relations_for_view, -> { includes(author: :status) } + scope :inc_statistics, -> { includes(:statistics) } scope :with_statistics, -> { joins(:statistics) } scope :inc_projects_namespace_route, -> { includes(project: [:route, :namespace]) } diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb index 09cac96e3a5..1dd650c8a90 100644 --- a/app/policies/group_member_policy.rb +++ b/app/policies/group_member_policy.rb @@ -4,7 +4,7 @@ class GroupMemberPolicy < BasePolicy delegate :group with_scope :subject - condition(:last_owner) { @subject.group.last_owner?(@subject.user) } + condition(:last_owner) { @subject.group.last_owner?(@subject.user) || @subject.group.last_blocked_owner?(@subject.user) } desc "Membership is users' own" with_score 0 diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index 59a9fb64a36..71cbe28de25 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -253,8 +253,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated nil, nil, { - 'toggle' => 'modal', - 'target' => '#modal-upload-blob' + 'target_branch' => default_branch_or_master, + 'original_branch' => default_branch_or_master, + 'can_push_code' => 'true', + 'path' => project_create_blob_path(project, default_branch_or_master), + 'project_path' => project.path } ) end diff --git a/app/services/packages/maven/metadata/base_create_xml_service.rb b/app/services/packages/maven/metadata/base_create_xml_service.rb new file mode 100644 index 00000000000..4d5cab4978e --- /dev/null +++ b/app/services/packages/maven/metadata/base_create_xml_service.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Packages + module Maven + module Metadata + class BaseCreateXmlService + include Gitlab::Utils::StrongMemoize + + INDENT_SPACE = 2 + + def initialize(metadata_content:, package:) + @metadata_content = metadata_content + @package = package + end + + private + + def xml_doc + strong_memoize(:xml_doc) do + Nokogiri::XML(@metadata_content) do |config| + config.default_xml.noblanks + end + end + end + + def xml_node(name, content) + xml_doc.create_element(name).tap { |e| e.content = content } + end + end + end + end +end diff --git a/app/services/packages/maven/metadata/create_plugins_xml_service.rb b/app/services/packages/maven/metadata/create_plugins_xml_service.rb new file mode 100644 index 00000000000..707a8c577ba --- /dev/null +++ b/app/services/packages/maven/metadata/create_plugins_xml_service.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module Packages + module Maven + module Metadata + class CreatePluginsXmlService < BaseCreateXmlService + XPATH_PLUGIN_ARTIFACT_ID = '//plugin/artifactId' + XPATH_PLUGINS = '//metadata/plugins' + EMPTY_PLUGINS_PAYLOAD = { + changes_exist: true, + empty_plugins: true + }.freeze + + def execute + return ServiceResponse.error(message: 'package not set') unless @package + return ServiceResponse.error(message: 'metadata_content not set') unless @metadata_content + return ServiceResponse.error(message: 'metadata_content is invalid') unless plugins_xml_node.present? + return ServiceResponse.success(payload: EMPTY_PLUGINS_PAYLOAD) if plugin_artifact_ids_from_database.empty? + + changes_exist = update_plugins_list + + payload = { changes_exist: changes_exist, empty_versions: false } + payload[:metadata_content] = xml_doc.to_xml(indent: INDENT_SPACE) if changes_exist + + ServiceResponse.success(payload: payload) + end + + private + + def update_plugins_list + return false if plugin_artifact_ids_from_xml == plugin_artifact_ids_from_database + + plugins_xml_node.children.remove + + plugin_artifact_ids_from_database.each do |artifact_id| + plugins_xml_node.add_child(plugin_node_for(artifact_id)) + end + + true + end + + def plugins_xml_node + strong_memoize(:plugins_xml_node) do + xml_doc.xpath(XPATH_PLUGINS) + .first + end + end + + def plugin_artifact_ids_from_xml + strong_memoize(:plugin_artifact_ids_from_xml) do + plugins_xml_node.xpath(XPATH_PLUGIN_ARTIFACT_ID) + .map(&:content) + end + end + + def plugin_artifact_ids_from_database + strong_memoize(:plugin_artifact_ids_from_database) do + package_names = plugin_artifact_ids_from_xml.map do |artifact_id| + "#{@package.name}/#{artifact_id}" + end + + packages = @package.project.packages + .maven + .displayable + .with_name(package_names) + .has_version + + ::Packages::Maven::Metadatum.for_package_ids(packages.select(:id)) + .order_created + .pluck_app_name + .uniq + end + end + + def plugin_node_for(artifact_id) + xml_doc.create_element('plugin').tap do |plugin_node| + plugin_node.add_child(xml_node('name', artifact_id)) + plugin_node.add_child(xml_node('prefix', prefix_from(artifact_id))) + plugin_node.add_child(xml_node('artifactId', artifact_id)) + end + end + + # Maven plugin prefix generation from + # https://github.com/apache/maven/blob/c3dba0e5ba71ee7cbd62620f669a8c206e71b5e2/maven-plugin-api/src/main/java/org/apache/maven/plugin/descriptor/PluginDescriptor.java#L189 + def prefix_from(artifact_id) + artifact_id.gsub(/-?maven-?/, '') + .gsub(/-?plugin-?/, '') + end + end + end + end +end diff --git a/app/services/packages/maven/metadata/create_versions_xml_service.rb b/app/services/packages/maven/metadata/create_versions_xml_service.rb index 6aebc1560c6..13b6efa8650 100644 --- a/app/services/packages/maven/metadata/create_versions_xml_service.rb +++ b/app/services/packages/maven/metadata/create_versions_xml_service.rb @@ -3,9 +3,7 @@ module Packages module Maven module Metadata - class CreateVersionsXmlService - include Gitlab::Utils::StrongMemoize - + class CreateVersionsXmlService < BaseCreateXmlService XPATH_VERSIONING = '//metadata/versioning' XPATH_VERSIONS = '//versions' XPATH_VERSION = '//version' @@ -13,18 +11,11 @@ module Packages XPATH_RELEASE = '//release' XPATH_LAST_UPDATED = '//lastUpdated' - INDENT_SPACE = 2 - EMPTY_VERSIONS_PAYLOAD = { changes_exist: true, empty_versions: true }.freeze - def initialize(metadata_content:, package:) - @metadata_content = metadata_content - @package = package - end - def execute return ServiceResponse.error(message: 'package not set') unless @package return ServiceResponse.error(message: 'metadata_content not set') unless @metadata_content @@ -57,7 +48,7 @@ module Packages version_xml_nodes.remove versions_from_database.each do |version| - versions_xml_node.add_child(version_node_for(version)) + versions_xml_node.add_child(xml_node('version', version)) end true end @@ -131,10 +122,6 @@ module Packages end end - def version_node_for(version) - Nokogiri::XML::Node.new('version', xml_doc).tap { |node| node.content = version } - end - def versions_from_xml strong_memoize(:versions_from_xml) do versions_xml_node.xpath(XPATH_VERSION) @@ -172,14 +159,6 @@ module Packages non_snapshot_versions_from_database.last end end - - def xml_doc - strong_memoize(:xml_doc) do - Nokogiri::XML(@metadata_content) do |config| - config.default_xml.noblanks - end - end - end end end end diff --git a/app/services/packages/maven/metadata/sync_service.rb b/app/services/packages/maven/metadata/sync_service.rb index ab45e30c4f7..a6534aa706d 100644 --- a/app/services/packages/maven/metadata/sync_service.rb +++ b/app/services/packages/maven/metadata/sync_service.rb @@ -16,26 +16,50 @@ module Packages return error('Non existing versionless package') unless versionless_package_for_versions return error('Non existing metadata file for versions') unless metadata_package_file_for_versions + if metadata_package_file_for_plugins + result = update_plugins_xml + + return result if result.error? + end + update_versions_xml end private def update_versions_xml - return error('Metadata file for versions is too big') if metadata_package_file_for_versions.size > MAX_FILE_SIZE + update_xml( + kind: :versions, + package_file: metadata_package_file_for_versions, + service_class: CreateVersionsXmlService, + payload_empty_field: :empty_versions + ) + end - metadata_package_file_for_versions.file.use_open_file do |file| - result = CreateVersionsXmlService.new(metadata_content: file, package: versionless_package_for_versions) - .execute + def update_plugins_xml + update_xml( + kind: :plugins, + package_file: metadata_package_file_for_plugins, + service_class: CreatePluginsXmlService, + payload_empty_field: :empty_plugins + ) + end + + def update_xml(kind:, package_file:, service_class:, payload_empty_field:) + return error("Metadata file for #{kind} is too big") if package_file.size > MAX_FILE_SIZE + + package_file.file.use_open_file do |file| + result = service_class.new(metadata_content: file, package: package_file.package) + .execute next result unless result.success? - next success('No changes for versions xml') unless result.payload[:changes_exist] + next success("No changes for #{kind} xml") unless result.payload[:changes_exist] - if result.payload[:empty_versions] - versionless_package_for_versions.destroy! - success('Versionless package for versions destroyed') + if result.payload[payload_empty_field] + package_file.package.destroy! + success("Versionless package for #{kind} destroyed") else - AppendPackageFileService.new(metadata_content: result.payload[:metadata_content], package: versionless_package_for_versions) + AppendPackageFileService.new(metadata_content: result.payload[:metadata_content], package: package_file.package) .execute end end @@ -43,28 +67,49 @@ module Packages def metadata_package_file_for_versions strong_memoize(:metadata_file_for_versions) do - versionless_package_for_versions.package_files - .with_file_name(Metadata.filename) - .recent - .first + metadata_package_file_for(versionless_package_for_versions) end end def versionless_package_for_versions strong_memoize(:versionless_package_for_versions) do - project.packages - .maven - .displayable - .with_name(package_name) - .with_version(nil) - .first + versionless_package_named(package_name) end end + def metadata_package_file_for_plugins + strong_memoize(:metadata_package_file_for_plugins) do + metadata_package_file_for(versionless_package_named(package_name_for_plugins)) + end + end + + def metadata_package_file_for(package) + return unless package + + package.package_files + .with_file_name(Metadata.filename) + .recent + .first + end + + def versionless_package_named(name) + project.packages + .maven + .displayable + .with_name(name) + .with_version(nil) + .first + end + def package_name params[:package_name] end + def package_name_for_plugins + group = versionless_package_for_versions.maven_metadatum.app_group + group.tr('.', '/') + end + def error(message) ServiceResponse.error(message: message) end diff --git a/app/views/admin/application_settings/_gitpod.html.haml b/app/views/admin/application_settings/_gitpod.html.haml index 531ddcdb86f..48b0c6be0a8 100644 --- a/app/views/admin/application_settings/_gitpod.html.haml +++ b/app/views/admin/application_settings/_gitpod.html.haml @@ -7,7 +7,7 @@ %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') %p - %integration-help-text{ "id" => "js-gitpod-settings-help-text", "message" => gitpod_enable_description, "message-url" => "https://gitpod.io/" } + #js-gitpod-settings-help-text{ data: {"message" => gitpod_enable_description, "message-url" => "https://gitpod.io/" } } = link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information') diff --git a/app/views/admin/labels/destroy.js.haml b/app/views/admin/labels/destroy.js.haml index b9b63829f25..5ee53088230 100644 --- a/app/views/admin/labels/destroy.js.haml +++ b/app/views/admin/labels/destroy.js.haml @@ -1,2 +1,3 @@ - if @labels.size == 0 - $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) + var emptyState = document.querySelector('.labels .nothing-here-block.hidden'); + if (emptyState) emptyState.classList.remove('hidden'); diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml index 7b737c49a87..6861a802a63 100644 --- a/app/views/admin/labels/index.html.haml +++ b/app/views/admin/labels/index.html.haml @@ -13,5 +13,6 @@ = render @labels = paginate @labels, theme: 'gitlab' - - else - .nothing-here-block= _('There are no labels yet') + + .nothing-here-block{ class: ('hidden' if @labels.present?) } + = _('There are no labels yet') diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index 5f0ecb2ee79..52cf0248f21 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -10,6 +10,8 @@ %ul.controls %li + = snippet_file_count(snippet) + %li = link_to gitlab_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if notes_count == 0) do = sprite_icon('comments', css_class: 'gl-vertical-align-text-bottom') = notes_count diff --git a/changelogs/unreleased/11424-sync-maven-metadata-for-maven-plugins.yml b/changelogs/unreleased/11424-sync-maven-metadata-for-maven-plugins.yml new file mode 100644 index 00000000000..b5163ef4b67 --- /dev/null +++ b/changelogs/unreleased/11424-sync-maven-metadata-for-maven-plugins.yml @@ -0,0 +1,5 @@ +--- +title: Support maven plugins packaging in the maven metadata sync worker +merge_request: 56229 +author: +type: fixed diff --git a/changelogs/unreleased/215043-include-number-of-files-in-snippet-lists.yml b/changelogs/unreleased/215043-include-number-of-files-in-snippet-lists.yml new file mode 100644 index 00000000000..f2eba09c5b1 --- /dev/null +++ b/changelogs/unreleased/215043-include-number-of-files-in-snippet-lists.yml @@ -0,0 +1,5 @@ +--- +title: Show number of files in snippet lists +merge_request: 55452 +author: +type: changed diff --git a/changelogs/unreleased/291994-remove-the-merge-manually-message-from-mrs.yml b/changelogs/unreleased/291994-remove-the-merge-manually-message-from-mrs.yml new file mode 100644 index 00000000000..78c71502452 --- /dev/null +++ b/changelogs/unreleased/291994-remove-the-merge-manually-message-from-mrs.yml @@ -0,0 +1,5 @@ +--- +title: Remove merge manually message +merge_request: 56016 +author: +type: fixed diff --git a/changelogs/unreleased/fix-prevent-to-remove-last-blocked-group-owner.yml b/changelogs/unreleased/fix-prevent-to-remove-last-blocked-group-owner.yml new file mode 100644 index 00000000000..2e8d05dc9b3 --- /dev/null +++ b/changelogs/unreleased/fix-prevent-to-remove-last-blocked-group-owner.yml @@ -0,0 +1,5 @@ +--- +title: Prevent removal of the last group owner if the last group owner is a blocked user +merge_request: 54587 +author: Jonas Wälter @wwwjon +type: fixed diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 2a42940482d..b395dc19681 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -86,8 +86,10 @@ Returns [`DevopsAdoptionSegmentConnection`](#devopsadoptionsegmentconnection). | ---- | ---- | ----------- | | `after` | [`String`](#string) | Returns the elements in the list that come after the specified cursor. | | `before` | [`String`](#string) | Returns the elements in the list that come before the specified cursor. | +| `directDescendantsOnly` | [`Boolean`](#boolean) | Limits segments to direct descendants of specified parent. | | `first` | [`Int`](#int) | Returns the first _n_ elements from the list. | | `last` | [`Int`](#int) | Returns the last _n_ elements from the list. | +| `parentNamespaceId` | [`NamespaceID`](#namespaceid) | Filter by ancestor namespace. | ### `echo` diff --git a/doc/user/application_security/api_fuzzing/img/api_fuzzing_configuration_snippet_v13.10.png b/doc/user/application_security/api_fuzzing/img/api_fuzzing_configuration_snippet_v13.10.png Binary files differnew file mode 100644 index 00000000000..80c550a3ae7 --- /dev/null +++ b/doc/user/application_security/api_fuzzing/img/api_fuzzing_configuration_snippet_v13.10.png diff --git a/doc/user/application_security/api_fuzzing/index.md b/doc/user/application_security/api_fuzzing/index.md index f2b43e0abd9..036da0068b5 100644 --- a/doc/user/application_security/api_fuzzing/index.md +++ b/doc/user/application_security/api_fuzzing/index.md @@ -70,6 +70,57 @@ GitLab 14.0 will require that you place API fuzzing configuration files (for exa repository's root. You can continue using your existing configuration files as they are, but starting in GitLab 14.0, GitLab will not check your repository's root for configuration files. +### Configuration form + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299234) in GitLab 13.10. +> - It's [deployed behind a feature flag](../../../user/feature_flags.md), enabled by default. +> - It's enabled on GitLab.com. +> - It's recommended for production use. +> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-api-fuzzing-configuration-form). **(ULTIMATE)** + +WARNING: +This feature might not be available to you. Check the **version history** note above for details. + +The API fuzzing configuration form helps you create or modify your project's API fuzzing +configuration. The form lets you choose values for the most common API fuzzing options and builds +a YAML snippet that you can paste in your GitLab CI/CD configuration. + +To generate an API Fuzzing configuration snippet: + +1. From your project's home page, go to **Security & Compliance > Configuration** in the left + sidebar. +1. Select **Configure** in the **API Fuzzing** row. +1. Complete the form as needed. Read below for more information on available configuration options. +1. Select **Generate code snippet**. + +A modal opens with the YAML snippet corresponding to the options you've selected in the form. + +![API Fuzzing configuration snippet](img/api_fuzzing_configuration_snippet_v13.10.png) + +Select **Copy code and open `.gitlab-ci.yml` file** to copy the snippet to your clipboard and be redirected +to your project's `.gitlab-ci.yml` file where you can paste the YAML configuration. + +Select **Copy code only** to copy the snippet to your clipboard and close the modal. + +#### Enable or disable API Fuzzing configuration form **(ULTIMATE)** + +The API Fuzzing configuration form is under development but ready for production use. +It is deployed behind a feature flag that is **enabled by default**. +[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) +can opt to disable it. + +To enable it: + +```ruby +Feature.enable(:api_fuzzing_configuration_ui) +``` + +To disable it: + +```ruby +Feature.disable(:api_fuzzing_configuration_ui) +``` + ### OpenAPI Specification > Support for OpenAPI Specification v3 was @@ -209,7 +260,8 @@ target API to test: FUZZAPI_PROFILE: Quick-10 ``` -1. Add the `FUZZAPI_HAR` variable and set it to the HAR file's location: +1. Provide the location of the HAR specification. You can provide the specification as a file + or URL. [URL support was introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285020) in GitLab 13.10 and later. Specify the location by adding the `FUZZAPI_HAR` variable: ```yaml include: @@ -300,7 +352,8 @@ information about the target API to test: FUZZAPI_PROFILE: Quick-10 ``` -1. Add the `FUZZAPI_POSTMAN_COLLECTION` variable and set it to the Postman Collection's location: +1. Provide the location of the Postman Collection specification. You can provide the specification as a file + or URL. [URL support was introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285020) in GitLab 13.10 and later. Specify the location by adding the `FUZZAPI_POSTMAN_COLLECTION` variable: ```yaml include: diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index d3a6e0f466a..b71cefbc7fe 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: reference, howto --- -# Static Application Security Testing (SAST) +# Static Application Security Testing (SAST) **(FREE)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3775) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.3. > - All open source (OSS) analyzers were moved to GitLab Free in GitLab 13.3. diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md index d248729b615..35374812b37 100644 --- a/doc/user/group/saml_sso/scim_setup.md +++ b/doc/user/group/saml_sso/scim_setup.md @@ -202,6 +202,10 @@ Upon the next sync, the user is deprovisioned, which means that the user is remo NOTE: Deprovisioning does not delete the user account. +During the synchronization process, all of your users get GitLab accounts, welcoming them +to their respective groups, with an invitation email. When implementing SCIM provisioning, +you may want to warn your security-conscious employees about this email. + ```mermaid graph TD A[Remove User from SCIM app] -->|IdP sends request to GitLab| B(GitLab: Is the user part of the group?) @@ -209,10 +213,6 @@ graph TD B -->|Yes| D[GitLab removes user from GitLab group] ``` -During the synchronization process, all of your users get GitLab accounts, welcoming them -to their respective groups, with an invitation email. When implementing SCIM provisioning, -you may want to warn your security-conscious employees about this email. - ## Troubleshooting This section contains possible solutions for problems you might encounter. diff --git a/doc/user/packages/index.md b/doc/user/packages/index.md index 03738eb0b9e..b35015d0b67 100644 --- a/doc/user/packages/index.md +++ b/doc/user/packages/index.md @@ -44,6 +44,7 @@ guides you through the process. | Conda | [#36891](https://gitlab.com/gitlab-org/gitlab/-/issues/36891) | | CRAN | [#36892](https://gitlab.com/gitlab-org/gitlab/-/issues/36892) | | Debian | [Draft: Merge Request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50438) | +| Helm | [#18997](https://gitlab.com/gitlab-org/gitlab/-/issues/18997) | | Opkg | [#36894](https://gitlab.com/gitlab-org/gitlab/-/issues/36894) | | P2 | [#36895](https://gitlab.com/gitlab-org/gitlab/-/issues/36895) | | Puppet | [#36897](https://gitlab.com/gitlab-org/gitlab/-/issues/36897) | diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3c3d0ea94dd..ae9f014fa09 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -208,6 +208,11 @@ msgid_plural "%d failed security jobs" msgstr[0] "" msgstr[1] "" +msgid "%d file" +msgid_plural "%d files" +msgstr[0] "" +msgstr[1] "" + msgid "%d fixed test result" msgid_plural "%d fixed test results" msgstr[0] "" @@ -8023,7 +8028,7 @@ msgstr "" msgid "Container does not exist" msgstr "" -msgid "Container must be a project." +msgid "Container must be a project or a group." msgstr "" msgid "Container registry images" @@ -36051,9 +36056,6 @@ msgstr "" msgid "mrWidget|Fork project merge requests do not create merge request pipelines that validate a post merge result unless invoked by a project member." msgstr "" -msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the" -msgstr "" - msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line" msgstr "" @@ -36204,18 +36206,12 @@ msgstr "" msgid "mrWidget|You can merge after removing denied licenses" msgstr "" -msgid "mrWidget|You can merge this merge request manually using the" -msgstr "" - msgid "mrWidget|Your password" msgstr "" msgid "mrWidget|branch does not exist." msgstr "" -msgid "mrWidget|command line" -msgstr "" - msgid "mrWidget|into" msgstr "" diff --git a/spec/frontend/blob/blob_file_dropzone_spec.js b/spec/frontend/blob/blob_file_dropzone_spec.js index e387ab7c18a..47c90030e18 100644 --- a/spec/frontend/blob/blob_file_dropzone_spec.js +++ b/spec/frontend/blob/blob_file_dropzone_spec.js @@ -1,10 +1,5 @@ import $ from 'jquery'; import BlobFileDropzone from '~/blob/blob_file_dropzone'; -import { trackUploadFileFormSubmitted } from '~/projects/upload_file_experiment'; - -jest.mock('~/projects/upload_file_experiment', () => ({ - trackUploadFileFormSubmitted: jest.fn(), -})); describe('BlobFileDropzone', () => { let dropzone; @@ -45,13 +40,5 @@ describe('BlobFileDropzone', () => { expect(replaceFileButton.is(':disabled')).toEqual(true); expect(dropzone.processQueue).toHaveBeenCalled(); }); - - it('calls the tracking event', () => { - jest.spyOn(window, 'alert').mockImplementation(() => {}); - - replaceFileButton.click(); - - expect(trackUploadFileFormSubmitted).toHaveBeenCalled(); - }); }); }); diff --git a/spec/frontend/projects/details/upload_button_spec.js b/spec/frontend/projects/details/upload_button_spec.js index d7308963088..ebb2b499ead 100644 --- a/spec/frontend/projects/details/upload_button_spec.js +++ b/spec/frontend/projects/details/upload_button_spec.js @@ -1,8 +1,11 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import UploadButton from '~/projects/details/upload_button.vue'; +import { trackFileUploadEvent } from '~/projects/upload_file_experiment_tracking'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; +jest.mock('~/projects/upload_file_experiment_tracking'); + const MODAL_ID = 'details-modal-upload-blob'; describe('UploadButton', () => { @@ -47,6 +50,10 @@ describe('UploadButton', () => { wrapper.find(GlButton).vm.$emit('click'); }); + it('tracks the click_upload_modal_trigger event', () => { + expect(trackFileUploadEvent).toHaveBeenCalledWith('click_upload_modal_trigger'); + }); + it('opens the modal', () => { expect(glModalDirective).toHaveBeenCalledWith(MODAL_ID); }); diff --git a/spec/frontend/projects/upload_file_experiment_spec.js b/spec/frontend/projects/upload_file_experiment_spec.js deleted file mode 100644 index aa1b1c44a31..00000000000 --- a/spec/frontend/projects/upload_file_experiment_spec.js +++ /dev/null @@ -1,52 +0,0 @@ -import ExperimentTracking from '~/experimentation/experiment_tracking'; -import * as UploadFileExperiment from '~/projects/upload_file_experiment'; - -jest.mock('~/experimentation/experiment_tracking'); - -const fixture = `<a class='js-upload-file-experiment-trigger' data-toggle='modal' data-target='#modal-upload-blob'></a><div id='modal-upload-blob'></div><div class='project-home-panel empty-project'></div>`; -const findModal = () => document.querySelector('[aria-modal="true"]'); -const findTrigger = () => document.querySelector('.js-upload-file-experiment-trigger'); - -beforeEach(() => { - document.body.innerHTML = fixture; -}); - -afterEach(() => { - document.body.innerHTML = ''; -}); - -describe('trackUploadFileFormSubmitted', () => { - it('initializes ExperimentTracking with the correct arguments and calls the tracking event with correct arguments', () => { - UploadFileExperiment.trackUploadFileFormSubmitted(); - - expect(ExperimentTracking).toHaveBeenCalledWith('empty_repo_upload', { - label: 'blob-upload-modal', - property: 'empty', - }); - expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith( - 'click_upload_modal_form_submit', - ); - }); - - it('initializes ExperimentTracking with the correct arguments when the project is not empty', () => { - document.querySelector('.empty-project').remove(); - - UploadFileExperiment.trackUploadFileFormSubmitted(); - - expect(ExperimentTracking).toHaveBeenCalledWith('empty_repo_upload', { - label: 'blob-upload-modal', - property: 'nonempty', - }); - }); -}); - -describe('initUploadFileTrigger', () => { - it('calls modal and tracks event', () => { - UploadFileExperiment.initUploadFileTrigger(); - - expect(findModal()).not.toExist(); - findTrigger().click(); - expect(findModal()).toExist(); - expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith('click_upload_modal_trigger'); - }); -}); diff --git a/spec/frontend/projects/upload_file_experiment_tracking_spec.js b/spec/frontend/projects/upload_file_experiment_tracking_spec.js new file mode 100644 index 00000000000..6817529e07e --- /dev/null +++ b/spec/frontend/projects/upload_file_experiment_tracking_spec.js @@ -0,0 +1,43 @@ +import ExperimentTracking from '~/experimentation/experiment_tracking'; +import { trackFileUploadEvent } from '~/projects/upload_file_experiment_tracking'; + +jest.mock('~/experimentation/experiment_tracking'); + +const eventName = 'click_upload_modal_form_submit'; +const fixture = `<a class='js-upload-file-experiment-trigger'></a><div class='project-home-panel empty-project'></div>`; + +beforeEach(() => { + document.body.innerHTML = fixture; +}); + +afterEach(() => { + document.body.innerHTML = ''; +}); + +describe('trackFileUploadEvent', () => { + it('initializes ExperimentTracking with the correct tracking event', () => { + trackFileUploadEvent(eventName); + + expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(eventName); + }); + + it('calls ExperimentTracking with the correct arguments', () => { + trackFileUploadEvent(eventName); + + expect(ExperimentTracking).toHaveBeenCalledWith('empty_repo_upload', { + label: 'blob-upload-modal', + property: 'empty', + }); + }); + + it('calls ExperimentTracking with the correct arguments when the project is not empty', () => { + document.querySelector('.empty-project').remove(); + + trackFileUploadEvent(eventName); + + expect(ExperimentTracking).toHaveBeenCalledWith('empty_repo_upload', { + label: 'blob-upload-modal', + property: 'nonempty', + }); + }); +}); diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js index 6e3cbad558d..935ed08f67a 100644 --- a/spec/frontend/repository/components/upload_blob_modal_spec.js +++ b/spec/frontend/repository/components/upload_blob_modal_spec.js @@ -6,9 +6,11 @@ import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import httpStatusCodes from '~/lib/utils/http_status'; import { visitUrl } from '~/lib/utils/url_utility'; +import { trackFileUploadEvent } from '~/projects/upload_file_experiment_tracking'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; +jest.mock('~/projects/upload_file_experiment_tracking'); jest.mock('~/flash'); jest.mock('~/lib/utils/url_utility', () => ({ visitUrl: jest.fn(), @@ -19,7 +21,7 @@ const initialProps = { modalId: 'upload-blob', commitMessage: 'Upload New File', targetBranch: 'master', - origionalBranch: 'master', + originalBranch: 'master', canPushCode: true, path: 'new_upload', }; @@ -160,6 +162,10 @@ describe('UploadBlobModal', () => { await waitForPromises(); }); + it('tracks the click_upload_modal_trigger event when opening the modal', () => { + expect(trackFileUploadEvent).toHaveBeenCalledWith('click_upload_modal_form_submit'); + }); + it('redirects to the uploaded file', () => { expect(visitUrl).toHaveBeenCalled(); }); @@ -179,6 +185,10 @@ describe('UploadBlobModal', () => { await waitForPromises(); }); + it('does not track an event', () => { + expect(trackFileUploadEvent).not.toHaveBeenCalled(); + }); + it('creates a flash error', () => { expect(createFlash).toHaveBeenCalledWith('Error uploading file. Please try again.'); }); diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_merge_help_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_merge_help_spec.js deleted file mode 100644 index 53a74bf7456..00000000000 --- a/spec/frontend/vue_mr_widget/components/mr_widget_merge_help_spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import MergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help.vue'; - -describe('MRWidgetMergeHelp', () => { - let wrapper; - - const createComponent = ({ props = {} } = {}) => { - wrapper = shallowMount(MergeHelpComponent, { - propsData: { - missingBranch: 'this-is-not-the-branch-you-are-looking-for', - ...props, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('with missing branch', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders missing branch information', () => { - expect(wrapper.find('.mr-widget-help').text()).toContain( - 'If the this-is-not-the-branch-you-are-looking-for branch exists in your local repository', - ); - }); - }); - - describe('without missing branch', () => { - beforeEach(() => { - createComponent({ - props: { missingBranch: '' }, - }); - }); - - it('renders information about how to merge manually', () => { - expect(wrapper.find('.mr-widget-help').text()).toContain( - 'You can merge this merge request manually', - ); - }); - }); -}); diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js index 7b020813bd5..c4962b608e1 100644 --- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js @@ -91,18 +91,6 @@ describe('MrWidgetOptions', () => { }); }); - describe('shouldRenderMergeHelp', () => { - it('should return false for the initial merged state', () => { - expect(wrapper.vm.shouldRenderMergeHelp).toBeFalsy(); - }); - - it('should return true for a state which requires help widget', () => { - wrapper.vm.mr.state = 'conflicts'; - - expect(wrapper.vm.shouldRenderMergeHelp).toBeTruthy(); - }); - }); - describe('shouldRenderPipelines', () => { it('should return true when hasCI is true', () => { wrapper.vm.mr.hasCI = true; diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 20983744919..24d09d1c035 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -565,6 +565,42 @@ RSpec.describe Group do end end + describe '#last_blocked_owner?' do + let(:blocked_user) { create(:user, :blocked) } + + before do + group.add_user(blocked_user, GroupMember::OWNER) + end + + it { expect(group.last_blocked_owner?(blocked_user)).to be_truthy } + + context 'with another active owner' do + before do + group.add_user(create(:user), GroupMember::OWNER) + end + + it { expect(group.last_blocked_owner?(blocked_user)).to be_falsy } + end + + context 'with 2 blocked owners' do + before do + group.add_user(create(:user, :blocked), GroupMember::OWNER) + end + + it { expect(group.last_blocked_owner?(blocked_user)).to be_falsy } + end + + context 'with owners from a parent' do + before do + parent_group = create(:group) + create(:group_member, :owner, group: parent_group) + group.update(parent: parent_group) + end + + it { expect(group.last_blocked_owner?(blocked_user)).to be_falsy } + end + end + describe '#lfs_enabled?' do context 'LFS enabled globally' do before do diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index e918a573e09..c41f466456f 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -130,14 +130,18 @@ RSpec.describe Member do @maintainer_user = create(:user).tap { |u| project.add_maintainer(u) } @maintainer = project.members.find_by(user_id: @maintainer_user.id) - @blocked_user = create(:user).tap do |u| + @blocked_maintainer_user = create(:user).tap do |u| project.add_maintainer(u) + + u.block! + end + @blocked_developer_user = create(:user).tap do |u| project.add_developer(u) u.block! end - @blocked_maintainer = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::MAINTAINER) - @blocked_developer = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::DEVELOPER) + @blocked_maintainer = project.members.find_by(user_id: @blocked_maintainer_user.id, access_level: Gitlab::Access::MAINTAINER) + @blocked_developer = project.members.find_by(user_id: @blocked_developer_user.id, access_level: Gitlab::Access::DEVELOPER) @invited_member = create(:project_member, :developer, project: project, @@ -161,7 +165,7 @@ RSpec.describe Member do describe '.access_for_user_ids' do it 'returns the right access levels' do - users = [@owner_user.id, @maintainer_user.id, @blocked_user.id] + users = [@owner_user.id, @maintainer_user.id, @blocked_maintainer_user.id] expected = { @owner_user.id => Gitlab::Access::OWNER, @maintainer_user.id => Gitlab::Access::MAINTAINER @@ -382,6 +386,20 @@ RSpec.describe Member do it { is_expected.not_to include @member_with_minimal_access } end + describe '.blocked' do + subject { described_class.blocked.to_a } + + it { is_expected.not_to include @owner } + it { is_expected.not_to include @maintainer } + it { is_expected.not_to include @invited_member } + it { is_expected.not_to include @accepted_invite_member } + it { is_expected.not_to include @requested_member } + it { is_expected.not_to include @accepted_request_member } + it { is_expected.to include @blocked_maintainer } + it { is_expected.to include @blocked_developer } + it { is_expected.not_to include @member_with_minimal_access } + end + describe '.active_without_invites_and_requests' do subject { described_class.active_without_invites_and_requests.to_a } diff --git a/spec/models/packages/maven/metadatum_spec.rb b/spec/models/packages/maven/metadatum_spec.rb index 16f6929d710..94a0e558985 100644 --- a/spec/models/packages/maven/metadatum_spec.rb +++ b/spec/models/packages/maven/metadatum_spec.rb @@ -36,5 +36,38 @@ RSpec.describe Packages::Maven::Metadatum, type: :model do expect(maven_metadatum.errors.to_a).to include('Package type must be Maven') end end + + context 'with a package' do + let_it_be(:package) { create(:package) } + + describe '.for_package_ids' do + let_it_be(:metadata) { create_list(:maven_metadatum, 3, package: package) } + + subject { Packages::Maven::Metadatum.for_package_ids(package.id) } + + it { is_expected.to match_array(metadata) } + end + + describe '.order_created' do + let_it_be(:metadatum1) { create(:maven_metadatum, package: package) } + let_it_be(:metadatum2) { create(:maven_metadatum, package: package) } + let_it_be(:metadatum3) { create(:maven_metadatum, package: package) } + let_it_be(:metadatum4) { create(:maven_metadatum, package: package) } + + subject { Packages::Maven::Metadatum.for_package_ids(package.id).order_created } + + it { is_expected.to eq([metadatum1, metadatum2, metadatum3, metadatum4]) } + end + + describe '.pluck_app_name' do + let_it_be(:metadatum1) { create(:maven_metadatum, package: package, app_name: 'one') } + let_it_be(:metadatum2) { create(:maven_metadatum, package: package, app_name: 'two') } + let_it_be(:metadatum3) { create(:maven_metadatum, package: package, app_name: 'three') } + + subject { Packages::Maven::Metadatum.for_package_ids(package.id).pluck_app_name } + + it { is_expected.to match_array([metadatum1, metadatum2, metadatum3].map(&:app_name)) } + end + end end end diff --git a/spec/policies/group_member_policy_spec.rb b/spec/policies/group_member_policy_spec.rb index 6099e4549b1..d283b0ffda5 100644 --- a/spec/policies/group_member_policy_spec.rb +++ b/spec/policies/group_member_policy_spec.rb @@ -90,6 +90,14 @@ RSpec.describe GroupMemberPolicy do specify { expect_allowed(:read_group) } end + context 'with one blocked owner' do + let(:owner) { create(:user, :blocked) } + let(:current_user) { owner } + + specify { expect_disallowed(*member_related_permissions) } + specify { expect_disallowed(:read_group) } + end + context 'with more than one owner' do let(:current_user) { owner } diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb index ff8bb820cc5..a9a5ecb3299 100644 --- a/spec/presenters/project_presenter_spec.rb +++ b/spec/presenters/project_presenter_spec.rb @@ -563,6 +563,51 @@ RSpec.describe ProjectPresenter do end end end + + describe '#upload_anchor_data' do + context 'with empty_repo_upload enabled' do + before do + stub_experiments(empty_repo_upload: :candidate) + end + + context 'user can push to branch' do + before do + project.add_developer(user) + end + + it 'returns upload_anchor_data' do + expect(presenter.upload_anchor_data).to have_attributes( + is_link: false, + label: a_string_including('Upload file'), + data: { + "can_push_code" => "true", + "original_branch" => "master", + "path" => "/#{project.full_path}/-/create/master", + "project_path" => project.path, + "target_branch" => "master" + } + ) + end + end + + context 'user cannot push to branch' do + it 'returns nil' do + expect(presenter.upload_anchor_data).to be_nil + end + end + end + + context 'with empty_repo_upload disabled' do + before do + stub_experiments(empty_repo_upload: :control) + project.add_developer(user) + end + + it 'returns nil' do + expect(presenter.upload_anchor_data).to be_nil + end + end + end end describe '#statistics_buttons' do diff --git a/spec/services/packages/maven/metadata/create_plugins_xml_service_spec.rb b/spec/services/packages/maven/metadata/create_plugins_xml_service_spec.rb new file mode 100644 index 00000000000..6fc1087940d --- /dev/null +++ b/spec/services/packages/maven/metadata/create_plugins_xml_service_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Packages::Maven::Metadata::CreatePluginsXmlService do + let_it_be(:group_id) { 'my/test' } + let_it_be(:package) { create(:maven_package, name: group_id, version: nil) } + + let(:plugins_in_database) { %w[one-maven-plugin two three-maven-plugin] } + let(:plugins_in_xml) { %w[one-maven-plugin two three-maven-plugin] } + let(:service) { described_class.new(metadata_content: metadata_xml, package: package) } + + describe '#execute' do + subject { service.execute } + + before do + next unless package + + plugins_in_database.each do |plugin| + create( + :maven_package, + name: "#{group_id}/#{plugin}", + version: '1.0.0', + project: package.project, + maven_metadatum_attributes: { + app_group: group_id.tr('/', '.'), + app_name: plugin, + app_version: '1.0.0' + } + ) + end + end + + shared_examples 'returning an xml with plugins from the database' do + it 'returns an metadata versions xml with versions in the database', :aggregate_failures do + expect(subject).to be_success + expect(subject.payload[:changes_exist]).to eq(true) + expect(subject.payload[:empty_versions]).to eq(false) + expect(plugins_from(subject.payload[:metadata_content])).to match_array(plugins_in_database) + end + end + + shared_examples 'returning no changes' do + it 'returns no changes', :aggregate_failures do + expect(subject).to be_success + expect(subject.payload).to eq(changes_exist: false, empty_versions: false) + end + end + + context 'with same plugins on both sides' do + it_behaves_like 'returning no changes' + end + + context 'with more plugins' do + let(:additional_plugins) { %w[four-maven-plugin five] } + + context 'in database' do + let(:plugins_in_database) { plugins_in_xml + additional_plugins } + + # we can't distinguish that the additional plugin are actually maven plugins + it_behaves_like 'returning no changes' + end + + context 'in xml' do + let(:plugins_in_xml) { plugins_in_database + additional_plugins } + + it_behaves_like 'returning an xml with plugins from the database' + end + end + + context 'with no versions in the database' do + let(:plugins_in_database) { [] } + + it 'returns a success', :aggregate_failures do + result = subject + + expect(result).to be_success + expect(result.payload).to eq(changes_exist: true, empty_plugins: true) + end + end + + context 'with an incomplete metadata content' do + let(:metadata_xml) { '<metadata></metadata>' } + + it_behaves_like 'returning an error service response', message: 'metadata_content is invalid' + end + + context 'with an invalid metadata content' do + let(:metadata_xml) { '<meta></metadata>' } + + it_behaves_like 'returning an error service response', message: 'metadata_content is invalid' + end + + it_behaves_like 'handling metadata content pointing to a file for the create xml service' + + it_behaves_like 'handling invalid parameters for create xml service' + end + + def metadata_xml + Nokogiri::XML::Builder.new do |xml| + xml.metadata do + xml.plugins do + plugins_in_xml.each do |plugin| + xml.plugin do + xml.name(plugin) + xml.prefix(prefix_from(plugin)) + xml.artifactId(plugin) + end + end + end + end + end.to_xml + end + + def prefix_from(artifact_id) + artifact_id.gsub(/-?maven-?/, '') + .gsub(/-?plugin-?/, '') + end + + def plugins_from(xml_content) + doc = Nokogiri::XML(xml_content) + doc.xpath('//metadata/plugins/plugin/artifactId').map(&:content) + end +end diff --git a/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb b/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb index 109f7adab4e..39c6feb5d12 100644 --- a/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb +++ b/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb @@ -181,59 +181,9 @@ RSpec.describe ::Packages::Maven::Metadata::CreateVersionsXmlService do it_behaves_like 'returning an error service response', message: 'metadata_content is invalid' end - context 'with metadata content pointing to a file' do - let(:service) { described_class.new(metadata_content: file, package: package) } - let(:file) do - Tempfile.new('metadata').tap do |file| - if file_contents - file.write(file_contents) - file.flush - file.rewind - end - end - end - - after do - file.close - file.unlink - end - - context 'with valid content' do - let(:file_contents) { metadata_xml } - - it 'returns no changes' do - result = subject - - expect(result).to be_success - expect(result.payload).to eq(changes_exist: false, empty_versions: false) - end - end + it_behaves_like 'handling metadata content pointing to a file for the create xml service' - context 'with invalid content' do - let(:file_contents) { '<meta></metadata>' } - - it_behaves_like 'returning an error service response', message: 'metadata_content is invalid' - end - - context 'with no content' do - let(:file_contents) { nil } - - it_behaves_like 'returning an error service response', message: 'metadata_content is invalid' - end - end - - context 'with no package' do - let(:metadata_xml) { '' } - let(:package) { nil } - - it_behaves_like 'returning an error service response', message: 'package not set' - end - - context 'with no metadata content' do - let(:metadata_xml) { nil } - - it_behaves_like 'returning an error service response', message: 'metadata_content not set' - end + it_behaves_like 'handling invalid parameters for create xml service' end def metadata_xml diff --git a/spec/services/packages/maven/metadata/sync_service_spec.rb b/spec/services/packages/maven/metadata/sync_service_spec.rb index be298e95236..f5634159e6d 100644 --- a/spec/services/packages/maven/metadata/sync_service_spec.rb +++ b/spec/services/packages/maven/metadata/sync_service_spec.rb @@ -57,97 +57,202 @@ RSpec.describe ::Packages::Maven::Metadata::SyncService do project.add_maintainer(user) end - context 'with no changes' do - let(:create_versions_xml_service_response) { ServiceResponse.success(payload: { changes_exist: false }) } - + context 'with a jar package' do before do - expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) + expect(::Packages::Maven::Metadata::CreatePluginsXmlService).not_to receive(:new) end - it_behaves_like 'returning a success service response', message: 'No changes for versions xml' - end + context 'with no changes' do + let(:create_versions_xml_service_response) { ServiceResponse.success(payload: { changes_exist: false }) } + + before do + expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) + end + + it_behaves_like 'returning a success service response', message: 'No changes for versions xml' + end + + context 'with changes' do + let(:create_versions_xml_service_response) { ServiceResponse.success(payload: { changes_exist: true, empty_versions: false, metadata_content: 'new metadata' }) } + + it_behaves_like 'returning a success service response', message: 'New metadata package files created' - context 'with changes' do - let(:create_versions_xml_service_response) { ServiceResponse.success(payload: { changes_exist: true, empty_versions: false, metadata_content: 'new metadata' }) } + context 'with empty versions' do + let(:create_versions_xml_service_response) { ServiceResponse.success(payload: { changes_exist: true, empty_versions: true }) } - it_behaves_like 'returning a success service response', message: 'New metadata package files created' + before do + expect(service.send(:versionless_package_for_versions)).to receive(:destroy!) + expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) + end - context 'with empty versions' do - let(:create_versions_xml_service_response) { ServiceResponse.success(payload: { changes_exist: true, empty_versions: true }) } + it_behaves_like 'returning a success service response', message: 'Versionless package for versions destroyed' + end + end + context 'with a too big maven metadata file for versions' do before do - expect(service.send(:versionless_package_for_versions)).to receive(:destroy!) - expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) + metadata_file_for_versions.update!(size: 100.megabytes) end - it_behaves_like 'returning a success service response', message: 'Versionless package for versions destroyed' + it_behaves_like 'returning an error service response', message: 'Metadata file for versions is too big' end - end - context 'with a too big maven metadata file for versions' do - before do - metadata_file_for_versions.update!(size: 100.megabytes) - end + context 'an error from the create versions xml service' do + let(:create_versions_xml_service_response) { ServiceResponse.error(message: 'metadata_content is invalid') } - it_behaves_like 'returning an error service response', message: 'Metadata file for versions is too big' - end + before do + expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) + end - context 'an error from the create versions xml service' do - let(:create_versions_xml_service_response) { ServiceResponse.error(message: 'metadata_content is invalid') } + it_behaves_like 'returning an error service response', message: 'metadata_content is invalid' + end - before do - expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) + context 'an error from the append package file service' do + let(:append_package_file_service_response) { ServiceResponse.error(message: 'metadata content is not set') } + + it_behaves_like 'returning an error service response', message: 'metadata content is not set' end - it_behaves_like 'returning an error service response', message: 'metadata_content is invalid' - end + context 'without a package name' do + let(:service) { described_class.new(container: project, current_user: user, params: { package_name: nil }) } - context 'an error from the append package file service' do - let(:append_package_file_service_response) { ServiceResponse.error(message: 'metadata content is not set') } + before do + expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) + expect(::Packages::Maven::Metadata::CreateVersionsXmlService).not_to receive(:new) + end - it_behaves_like 'returning an error service response', message: 'metadata content is not set' - end + it_behaves_like 'returning an error service response', message: 'Blank package name' + end - context 'without a package name' do - let(:service) { described_class.new(container: project, current_user: user, params: { package_name: nil }) } + context 'without a versionless package for version' do + before do + versionless_package_for_versions.update!(version: '2.2.2') + expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) + expect(::Packages::Maven::Metadata::CreateVersionsXmlService).not_to receive(:new) + end - before do - expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) - expect(::Packages::Maven::Metadata::CreateVersionsXmlService).not_to receive(:new) + it_behaves_like 'returning an error service response', message: 'Non existing versionless package' end - it_behaves_like 'returning an error service response', message: 'Blank package name' - end + context 'without a metadata package file for versions' do + before do + versionless_package_for_versions.package_files.update_all(file_name: 'test.txt') + expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) + expect(::Packages::Maven::Metadata::CreateVersionsXmlService).not_to receive(:new) + end - context 'without a versionless package for version' do - before do - versionless_package_for_versions.update!(version: '2.2.2') - expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) - expect(::Packages::Maven::Metadata::CreateVersionsXmlService).not_to receive(:new) + it_behaves_like 'returning an error service response', message: 'Non existing metadata file for versions' end - it_behaves_like 'returning an error service response', message: 'Non existing versionless package' + context 'without a project' do + let(:service) { described_class.new(container: nil, current_user: user, params: { package_name: versionless_package_for_versions.name }) } + + before do + expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) + expect(::Packages::Maven::Metadata::CreateVersionsXmlService).not_to receive(:new) + end + + it_behaves_like 'returning an error service response', message: 'Not allowed' + end end - context 'without a metadata package file for versions' do + context 'with a maven plugin package' do + let_it_be(:versionless_package_name_for_plugins) { versionless_package_for_versions.maven_metadatum.app_group.tr('.', '/') } + let_it_be_with_reload(:versionless_package_for_plugins) { create(:maven_package, name: versionless_package_name_for_plugins, version: nil, project: project) } + let_it_be_with_reload(:metadata_file_for_plugins) { create(:package_file, :xml, package: versionless_package_for_plugins) } + + let(:create_plugins_xml_service_double) { double(::Packages::Maven::Metadata::CreatePluginsXmlService, execute: create_plugins_xml_service_response) } + let(:create_plugins_xml_service_response) { ServiceResponse.success(payload: { changes_exist: false }) } + before do - versionless_package_for_versions.package_files.update_all(file_name: 'test.txt') - expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) - expect(::Packages::Maven::Metadata::CreateVersionsXmlService).not_to receive(:new) + allow(::Packages::Maven::Metadata::CreatePluginsXmlService) + .to receive(:new).with(metadata_content: an_instance_of(ObjectStorage::Concern::OpenFile), package: versionless_package_for_plugins).and_return(create_plugins_xml_service_double) + allow(::Packages::Maven::Metadata::AppendPackageFileService) + .to receive(:new).with(metadata_content: an_instance_of(String), package: versionless_package_for_plugins).and_return(append_package_file_service_double) end - it_behaves_like 'returning an error service response', message: 'Non existing metadata file for versions' - end + context 'with no changes' do + let(:create_versions_xml_service_response) { ServiceResponse.success(payload: { changes_exist: false }) } - context 'without a project' do - let(:service) { described_class.new(container: nil, current_user: user, params: { package_name: versionless_package_for_versions.name }) } + before do + expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) + end - before do - expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) - expect(::Packages::Maven::Metadata::CreateVersionsXmlService).not_to receive(:new) + it_behaves_like 'returning a success service response', message: 'No changes for versions xml' end - it_behaves_like 'returning an error service response', message: 'Not allowed' + context 'with changes in the versions xml' do + let(:create_versions_xml_service_response) { ServiceResponse.success(payload: { changes_exist: true, empty_versions: false, metadata_content: 'new metadata' }) } + + it_behaves_like 'returning a success service response', message: 'New metadata package files created' + + context 'with changes in the plugin xml' do + let(:create_plugins_xml_service_response) { ServiceResponse.success(payload: { changes_exist: true, empty_plugins: false, metadata_content: 'new metadata' }) } + + it_behaves_like 'returning a success service response', message: 'New metadata package files created' + end + + context 'with empty versions' do + let(:create_versions_xml_service_response) { ServiceResponse.success(payload: { changes_exist: true, empty_versions: true }) } + let(:create_plugins_xml_service_response) { ServiceResponse.success(payload: { changes_exist: true, empty_plugins: true }) } + + before do + expect(service.send(:versionless_package_for_versions)).to receive(:destroy!) + expect(service.send(:metadata_package_file_for_plugins).package).to receive(:destroy!) + expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) + end + + it_behaves_like 'returning a success service response', message: 'Versionless package for versions destroyed' + end + + context 'with a too big maven metadata file for versions' do + before do + metadata_file_for_plugins.update!(size: 100.megabytes) + end + + it_behaves_like 'returning an error service response', message: 'Metadata file for plugins is too big' + end + + context 'an error from the create versions xml service' do + let(:create_plugins_xml_service_response) { ServiceResponse.error(message: 'metadata_content is invalid') } + + before do + expect(::Packages::Maven::Metadata::CreateVersionsXmlService).not_to receive(:new) + expect(::Packages::Maven::Metadata::AppendPackageFileService).not_to receive(:new) + end + + it_behaves_like 'returning an error service response', message: 'metadata_content is invalid' + end + + context 'an error from the append package file service' do + let(:create_plugins_xml_service_response) { ServiceResponse.success(payload: { changes_exist: true, empty_plugins: false, metadata_content: 'new metadata' }) } + let(:append_package_file_service_response) { ServiceResponse.error(message: 'metadata content is not set') } + + before do + expect(::Packages::Maven::Metadata::CreateVersionsXmlService).not_to receive(:new) + end + + it_behaves_like 'returning an error service response', message: 'metadata content is not set' + end + + context 'without a versionless package for plugins' do + before do + versionless_package_for_plugins.package_files.update_all(file_name: 'test.txt') + expect(::Packages::Maven::Metadata::CreatePluginsXmlService).not_to receive(:new) + end + + it_behaves_like 'returning a success service response', message: 'New metadata package files created' + end + + context 'without a metadata package file for plugins' do + before do + versionless_package_for_plugins.package_files.update_all(file_name: 'test.txt') + expect(::Packages::Maven::Metadata::CreatePluginsXmlService).not_to receive(:new) + end + + it_behaves_like 'returning a success service response', message: 'New metadata package files created' + end + end end end end diff --git a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb index 7daebcbb0fe..00d3bd08218 100644 --- a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb +++ b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb @@ -97,18 +97,16 @@ end RSpec.shared_examples 'uploads and commits a new text file via "upload file" button' do it 'uploads and commits a new text file via "upload file" button', :js do - find('.js-upload-file-experiment-trigger', text: 'Upload file').click + find('[data-testid="upload-file-button"]').click - drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) + attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true) - page.within('#modal-upload-blob') do + page.within('#details-modal-upload-blob') do fill_in(:commit_message, with: 'New commit message') end click_button('Upload file') - wait_for_requests - expect(page).to have_content('New commit message') expect(page).to have_content('Lorem ipsum dolor sit amet') expect(page).to have_content('Sed ut perspiciatis unde omnis') diff --git a/spec/support/shared_examples/services/packages/maven/metadata_shared_examples.rb b/spec/support/shared_examples/services/packages/maven/metadata_shared_examples.rb new file mode 100644 index 00000000000..4de672bb732 --- /dev/null +++ b/spec/support/shared_examples/services/packages/maven/metadata_shared_examples.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'handling metadata content pointing to a file for the create xml service' do + context 'with metadata content pointing to a file' do + let(:service) { described_class.new(metadata_content: file, package: package) } + let(:file) do + Tempfile.new('metadata').tap do |file| + if file_contents + file.write(file_contents) + file.flush + file.rewind + end + end + end + + after do + file.close + file.unlink + end + + context 'with valid content' do + let(:file_contents) { metadata_xml } + + it 'returns no changes' do + expect(subject).to be_success + expect(subject.payload).to eq(changes_exist: false, empty_versions: false) + end + end + + context 'with invalid content' do + let(:file_contents) { '<meta></metadata>' } + + it_behaves_like 'returning an error service response', message: 'metadata_content is invalid' + end + + context 'with no content' do + let(:file_contents) { nil } + + it_behaves_like 'returning an error service response', message: 'metadata_content is invalid' + end + end +end + +RSpec.shared_examples 'handling invalid parameters for create xml service' do + context 'with no package' do + let(:metadata_xml) { '' } + let(:package) { nil } + + it_behaves_like 'returning an error service response', message: 'package not set' + end + + context 'with no metadata content' do + let(:metadata_xml) { nil } + + it_behaves_like 'returning an error service response', message: 'metadata_content not set' + end +end diff --git a/spec/views/shared/snippets/_snippet.html.haml_spec.rb b/spec/views/shared/snippets/_snippet.html.haml_spec.rb new file mode 100644 index 00000000000..712021ec1e1 --- /dev/null +++ b/spec/views/shared/snippets/_snippet.html.haml_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'shared/snippets/_snippet.html.haml' do + let_it_be(:snippet) { create(:snippet) } + + before do + allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings) + allow(view).to receive(:can?) { true } + + @noteable_meta_data = Class.new { include Gitlab::NoteableMetadata }.new.noteable_meta_data([snippet], 'Snippet') + end + + context 'snippet with statistics' do + it 'renders correct file count and tooltip' do + snippet.statistics.file_count = 3 + + render 'shared/snippets/snippet', snippet: snippet + + expect(rendered).to have_selector("span.file_count", text: '3') + expect(rendered).to have_selector("span.file_count[title=\"3 files\"]") + end + + it 'renders correct file count and tooltip when file_count is 1' do + snippet.statistics.file_count = 1 + + render 'shared/snippets/snippet', snippet: snippet + + expect(rendered).to have_selector("span.file_count", text: '1') + expect(rendered).to have_selector("span.file_count[title=\"1 file\"]") + end + + it 'does not render file count when file count is 0' do + snippet.statistics.file_count = 0 + + render 'shared/snippets/snippet', snippet: snippet + + expect(rendered).not_to have_selector('span.file_count') + end + end + + context 'snippet without statistics' do + it 'does not render file count if statistics are not present' do + snippet.statistics = nil + + render 'shared/snippets/snippet', snippet: snippet + + expect(rendered).not_to have_selector('span.file_count') + end + end +end diff --git a/spec/workers/packages/maven/metadata/sync_worker_spec.rb b/spec/workers/packages/maven/metadata/sync_worker_spec.rb index e3e0e598264..7e0f3616491 100644 --- a/spec/workers/packages/maven/metadata/sync_worker_spec.rb +++ b/spec/workers/packages/maven/metadata/sync_worker_spec.rb @@ -22,18 +22,23 @@ RSpec.describe Packages::Maven::Metadata::SyncWorker, type: :worker do subject { worker.perform(user.id, project.id, package_name) } - context 'with a valid package name' do - before do - file = CarrierWaveStringFile.new_file(file_content: versions_xml_content, filename: 'maven-metadata.xml', content_type: 'application/xml') - metadata_package_file.update!(file: file) - - versions.each do |version| - create(:maven_package, name: versionless_package_for_versions.name, version: version, project: versionless_package_for_versions.project) + context 'with a jar' do + context 'with a valid package name' do + before do + metadata_package_file.update!( + file: CarrierWaveStringFile.new_file( + file_content: versions_xml_content, + filename: 'maven-metadata.xml', + content_type: 'application/xml' + ) + ) + + versions.each do |version| + create(:maven_package, name: versionless_package_for_versions.name, version: version, project: versionless_package_for_versions.project) + end end - end - context 'idempotent worker' do - include_examples 'an idempotent worker' do + it_behaves_like 'an idempotent worker' do let(:job_args) { [user.id, project.id, package_name] } it 'creates the updated metadata files', :aggregate_failures do @@ -45,31 +50,116 @@ RSpec.describe Packages::Maven::Metadata::SyncWorker, type: :worker do expect(most_recent_versions.versions).to match_array(versions) end end - end - it 'logs the message from the service' do - expect(worker).to receive(:log_extra_metadata_on_done).with(:message, 'New metadata package file created') + it 'logs the message from the service' do + expect(worker).to receive(:log_extra_metadata_on_done).with(:message, 'New metadata package file created') - subject - end + subject + end - context 'not in the passed project' do - let(:project) { create(:project) } + context 'not in the passed project' do + let(:project) { create(:project) } - it 'does not create the updated metadata files' do - expect { subject } - .to change { ::Packages::PackageFile.count }.by(0) - .and raise_error(described_class::SyncError, 'Non existing versionless package') + it 'does not create the updated metadata files' do + expect { subject } + .to change { ::Packages::PackageFile.count }.by(0) + .and raise_error(described_class::SyncError, 'Non existing versionless package') + end + end + + context 'with a user with not enough permissions' do + let(:role) { :guest } + + it 'does not create the updated metadata files' do + expect { subject } + .to change { ::Packages::PackageFile.count }.by(0) + .and raise_error(described_class::SyncError, 'Not allowed') + end end end + end - context 'with a user with not enough permissions' do - let(:role) { :guest } + context 'with a maven plugin' do + let_it_be(:versionless_package_name_for_plugins) { versionless_package_for_versions.maven_metadatum.app_group.tr('.', '/') } + let_it_be(:versionless_package_for_versions) { create(:maven_package, name: "#{versionless_package_name_for_plugins}/one-maven-plugin", version: nil) } + let_it_be(:metadata_package_file) { create(:package_file, :xml, package: versionless_package_for_versions) } + + let_it_be(:versionless_package_for_plugins) { create(:maven_package, name: versionless_package_name_for_plugins, version: nil, project: versionless_package_for_versions.project) } + let_it_be(:metadata_package_file_for_plugins) { create(:package_file, :xml, package: versionless_package_for_plugins) } + + let_it_be(:addtional_maven_package_for_same_group_id) { create(:maven_package, name: "#{versionless_package_name_for_plugins}/maven-package", project: versionless_package_for_versions.project) } + + let(:plugins) { %w[one-maven-plugin three-maven-plugin] } + let(:most_recent_metadata_file_for_plugins) { versionless_package_for_plugins.package_files.recent.with_file_name(Packages::Maven::Metadata.filename).first } + + context 'with a valid package name' do + before do + versionless_package_for_versions.update!(name: package_name) + + metadata_package_file.update!( + file: CarrierWaveStringFile.new_file( + file_content: versions_xml_content, + filename: 'maven-metadata.xml', + content_type: 'application/xml' + ) + ) + + metadata_package_file_for_plugins.update!( + file: CarrierWaveStringFile.new_file( + file_content: plugins_xml_content, + filename: 'maven-metadata.xml', + content_type: 'application/xml' + ) + ) + + plugins.each do |plugin| + versions.each do |version| + pkg = create(:maven_package, name: "#{versionless_package_name_for_plugins}/#{plugin}", version: version, project: versionless_package_for_versions.project) + pkg.maven_metadatum.update!(app_name: plugin) + end + end + end + + it_behaves_like 'an idempotent worker' do + let(:job_args) { [user.id, project.id, package_name] } + + it 'creates the updated metadata files', :aggregate_failures do + expect { subject }.to change { ::Packages::PackageFile.count }.by(5 * 2) # the two xml files are updated + + most_recent_versions = versions_from(most_recent_metadata_file_for_versions.file.read) + expect(most_recent_versions.latest).to eq('3.0-SNAPSHOT') + expect(most_recent_versions.release).to eq('2.1') + expect(most_recent_versions.versions).to match_array(versions) - it 'does not create the updated metadata files' do - expect { subject } - .to change { ::Packages::PackageFile.count }.by(0) - .and raise_error(described_class::SyncError, 'Not allowed') + plugins_from_xml = plugins_from(most_recent_metadata_file_for_plugins.file.read) + expect(plugins_from_xml).to match_array(plugins) + end + end + + it 'logs the message from the service' do + expect(worker).to receive(:log_extra_metadata_on_done).with(:message, 'New metadata package file created') + + subject + end + + context 'not in the passed project' do + let(:project) { create(:project) } + + it 'does not create the updated metadata files' do + expect { subject } + .to change { ::Packages::PackageFile.count }.by(0) + .and raise_error(described_class::SyncError, 'Non existing versionless package') + end + end + + context 'with a user with not enough permissions' do + let(:role) { :guest } + + it 'does not create the updated metadata files' do + expect { subject } + .to change { ::Packages::PackageFile.count }.by(0) + .and raise_error(described_class::SyncError, 'Not allowed') + end end end end @@ -112,6 +202,12 @@ RSpec.describe Packages::Maven::Metadata::SyncWorker, type: :worker do ) end + def plugins_from(xml_content) + xml_doc = Nokogiri::XML(xml_content) + + xml_doc.xpath('//metadata/plugins/plugin/name').map(&:content) + end + def versions_xml_content Nokogiri::XML::Builder.new do |xml| xml.metadata do @@ -130,4 +226,28 @@ RSpec.describe Packages::Maven::Metadata::SyncWorker, type: :worker do end end.to_xml end + + def plugins_xml_content + Nokogiri::XML::Builder.new do |xml| + xml.metadata do + xml.plugins do + xml.plugin do + xml.name('one-maven-plugin') + xml.prefix('one') + xml.artifactId('one-maven-plugin') + end + xml.plugin do + xml.name('two-maven-plugin') + xml.prefix('two') + xml.artifactId('two-maven-plugin') + end + xml.plugin do + xml.name('three-maven-plugin') + xml.prefix('three') + xml.artifactId('three-maven-plugin') + end + end + end + end.to_xml + end end |