diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-16 09:07:51 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-16 09:07:51 +0000 |
commit | 914ea32e0efca21436220df2c10e1bfbe4ed3da9 (patch) | |
tree | e8eb3b97aea2006bd863c586b7ec41d51f654b3b | |
parent | 3546e1bb0971347e9e9984de0799e3fb53743b33 (diff) | |
download | gitlab-ce-914ea32e0efca21436220df2c10e1bfbe4ed3da9.tar.gz |
Add latest changes from gitlab-org/gitlab@master
100 files changed, 1177 insertions, 284 deletions
diff --git a/Dangerfile b/Dangerfile index 228190cd530..b65a9074078 100644 --- a/Dangerfile +++ b/Dangerfile @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative 'lib/gitlab_danger' +require_relative 'lib/gitlab/danger/request_helper' danger.import_plugin('danger/plugins/helper.rb') danger.import_plugin('danger/plugins/roulette.rb') diff --git a/app/assets/javascripts/issuable_sidebar/components/sidebar_app.vue b/app/assets/javascripts/issuable_sidebar/components/sidebar_app.vue new file mode 100644 index 00000000000..06c50f62aab --- /dev/null +++ b/app/assets/javascripts/issuable_sidebar/components/sidebar_app.vue @@ -0,0 +1,23 @@ +<script> +export default { + props: { + signedIn: { + type: Boolean, + required: true, + }, + sidebarStatusClass: { + type: String, + required: false, + default: '', + }, + }, +}; +</script> + +<template> + <aside + :class="sidebarStatusClass" + class="right-sidebar js-right-sidebar js-issuable-sidebar" + aria-live="polite" + ></aside> +</template> diff --git a/app/assets/javascripts/issuable_sidebar/sidebar_bundle.js b/app/assets/javascripts/issuable_sidebar/sidebar_bundle.js new file mode 100644 index 00000000000..c8acafa8cd8 --- /dev/null +++ b/app/assets/javascripts/issuable_sidebar/sidebar_bundle.js @@ -0,0 +1,27 @@ +import Vue from 'vue'; + +import SidebarApp from './components/sidebar_app.vue'; + +export default () => { + const el = document.getElementById('js-vue-issuable-sidebar'); + + if (!el) { + return false; + } + + const { sidebarStatusClass } = el.dataset; + // An empty string is present when user is signed in. + const signedIn = el.dataset.signedIn === ''; + + return new Vue({ + el, + components: { SidebarApp }, + render: createElement => + createElement('sidebar-app', { + props: { + signedIn, + sidebarStatusClass, + }, + }), + }); +}; diff --git a/app/assets/javascripts/jobs/components/log/duration_badge.vue b/app/assets/javascripts/jobs/components/log/duration_badge.vue index 31a101d2c95..8e5dcdcc902 100644 --- a/app/assets/javascripts/jobs/components/log/duration_badge.vue +++ b/app/assets/javascripts/jobs/components/log/duration_badge.vue @@ -9,7 +9,7 @@ export default { }; </script> <template> - <div class="log-duration-badge rounded align-self-start px-2 ml-2 flex-shrink-0"> + <div class="log-duration-badge rounded align-self-start px-2 ml-2 flex-shrink-0 ws-normal"> {{ duration }} </div> </template> diff --git a/app/assets/javascripts/jobs/components/log/line.vue b/app/assets/javascripts/jobs/components/log/line.vue index 9fae541125e..33ee84bd4ee 100644 --- a/app/assets/javascripts/jobs/components/log/line.vue +++ b/app/assets/javascripts/jobs/components/log/line.vue @@ -21,8 +21,12 @@ export default { <template> <div class="js-line log-line"> <line-number :line-number="line.lineNumber" :path="path" /> - <span v-for="(content, i) in line.content" :key="i" :class="content.style">{{ - content.text - }}</span> + <span + v-for="(content, i) in line.content" + :key="i" + :class="content.style" + class="ws-pre-wrap" + >{{ content.text }}</span + > </div> </template> diff --git a/app/assets/javascripts/jobs/components/log/line_header.vue b/app/assets/javascripts/jobs/components/log/line_header.vue index 92cf3b3cf5f..85ccd5996b5 100644 --- a/app/assets/javascripts/jobs/components/log/line_header.vue +++ b/app/assets/javascripts/jobs/components/log/line_header.vue @@ -43,15 +43,19 @@ export default { <template> <div - class="log-line collapsible-line d-flex justify-content-between" + class="log-line collapsible-line d-flex justify-content-between ws-normal" role="button" @click="handleOnClick" > <icon :name="iconName" class="arrow position-absolute" /> <line-number :line-number="line.lineNumber" :path="path" /> - <span v-for="(content, i) in line.content" :key="i" class="line-text" :class="content.style">{{ - content.text - }}</span> + <span + v-for="(content, i) in line.content" + :key="i" + class="line-text w-100 ws-pre-wrap" + :class="content.style" + >{{ content.text }}</span + > <duration-badge v-if="duration" :duration="duration" /> </div> </template> diff --git a/app/assets/javascripts/jobs/components/log/line_number.vue b/app/assets/javascripts/jobs/components/log/line_number.vue index 08c4a7ed330..ae96c32874b 100644 --- a/app/assets/javascripts/jobs/components/log/line_number.vue +++ b/app/assets/javascripts/jobs/components/log/line_number.vue @@ -48,7 +48,7 @@ export default { <template> <gl-link :id="lineNumberId" - class="d-inline-block text-right line-number" + class="d-inline-block text-right line-number flex-shrink-0" :href="buildLineNumber" >{{ parsedLineNumber }}</gl-link > diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index 412ae146ca0..702f00888d0 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -19,7 +19,7 @@ export default { state.isSidebarOpen = true; }, - [types.RECEIVE_TRACE_SUCCESS](state, log) { + [types.RECEIVE_TRACE_SUCCESS](state, log = {}) { if (log.state) { state.traceState = log.state; } diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 9ecb9324f8c..09afa16e283 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -416,7 +416,6 @@ export default { <gl-button v-if="showRearrangePanelsBtn" :pressed="isRearrangingPanels" - new-style variant="default" class="mr-2 mt-1 js-rearrange-button" @click="toggleRearrangingPanels" @@ -426,7 +425,6 @@ export default { <gl-button v-if="addingMetricsAvailable" v-gl-modal="$options.addMetric.modalId" - new-style variant="outline-success" class="mr-2 mt-1 js-add-metric-button" > diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index 0447d1f79fb..28a136a5fa5 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -5,6 +5,7 @@ import ZenMode from '~/zen_mode'; import '~/notes/index'; import initIssueableApp from '~/issue_show'; import initRelatedMergeRequestsApp from '~/related_merge_requests'; +import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle'; export default function() { initIssueableApp(); @@ -12,5 +13,9 @@ export default function() { new Issue(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new - initIssuableSidebar(); + if (gon.features && gon.features.vueIssuableSidebar) { + initVueIssuableSidebarApp(); + } else { + initIssuableSidebar(); + } } diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js index 7968dfd7a12..ce74a6de11f 100644 --- a/app/assets/javascripts/pages/projects/issues/show/index.js +++ b/app/assets/javascripts/pages/projects/issues/show/index.js @@ -3,5 +3,7 @@ import initShow from '../show'; document.addEventListener('DOMContentLoaded', () => { initShow(); - initSidebarBundle(); + if (gon.features && !gon.features.vueIssuableSidebar) { + initSidebarBundle(); + } }); diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js index 7bfb83a2204..fa1de1f13cb 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js @@ -4,11 +4,16 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import { handleLocationHash } from '~/lib/utils/common_utils'; import howToMerge from '~/how_to_merge'; import initPipelines from '~/commit/pipelines/pipelines_bundle'; +import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle'; import initWidget from '../../../vue_merge_request_widget'; export default function() { new ZenMode(); // eslint-disable-line no-new - initIssuableSidebar(); + if (gon.features && gon.features.vueIssuableSidebar) { + initVueIssuableSidebarApp(); + } else { + initIssuableSidebar(); + } initPipelines(); new ShortcutsIssuable(true); // eslint-disable-line no-new handleLocationHash(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js index f61f4db78d5..ddc648702f1 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js @@ -4,6 +4,8 @@ import initShow from '../init_merge_request_show'; document.addEventListener('DOMContentLoaded', () => { initShow(); - initSidebarBundle(); + if (gon.features && !gon.features.vueIssuableSidebar) { + initSidebarBundle(); + } initMrNotes(); }); diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 16cb63fc0df..370fc84e492 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -440,6 +440,7 @@ img.emoji { .flex-no-shrink { flex-shrink: 0; } .ws-initial { white-space: initial; } .ws-normal { white-space: normal; } +.ws-pre-wrap { white-space: pre-wrap; } .overflow-auto { overflow: auto; } .d-flex-center { diff --git a/app/assets/stylesheets/framework/job_log.scss b/app/assets/stylesheets/framework/job_log.scss index f26c475c3c1..4a57a458c50 100644 --- a/app/assets/stylesheets/framework/job_log.scss +++ b/app/assets/stylesheets/framework/job_log.scss @@ -9,7 +9,6 @@ border-radius: $border-radius-small; min-height: 42px; background-color: $builds-trace-bg; - white-space: pre-wrap; } .log-line { diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 3204e1e388b..35e364abba3 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -104,7 +104,6 @@ class GroupsController < Groups::ApplicationController redirect_to edit_group_path(@group, anchor: params[:update_section]), notice: "Group '#{@group.name}' was successfully updated." else @group.path = @group.path_before_last_save || @group.path_was - render action: "edit" end end @@ -124,7 +123,7 @@ class GroupsController < Groups::ApplicationController flash[:notice] = "Group '#{@group.name}' was successfully transferred." redirect_to group_path(@group) else - flash[:alert] = service.error + flash[:alert] = service.error.html_safe redirect_to edit_group_path(@group) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 7a192a9ec2d..96cb400950b 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -42,6 +42,10 @@ class Projects::IssuesController < Projects::ApplicationController before_action :authorize_import_issues!, only: [:import_csv] before_action :authorize_download_code!, only: [:related_branches] + before_action do + push_frontend_feature_flag(:vue_issuable_sidebar, project.group) + end + respond_to :html alias_method :designs, :show diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index bd22226da5c..ff199e05e99 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -21,6 +21,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:diffs_batch_load, @project) end + before_action do + push_frontend_feature_flag(:vue_issuable_sidebar, @project.group) + end + around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions] def index diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 1bacdc0b1b2..106ef1b72c1 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -184,7 +184,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def show_represent_params - { grouped: true } + { grouped: true, expanded: params[:expanded].to_a.map(&:to_i) } end def create_params diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 3dacd6a6224..b016aa8e477 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -42,6 +42,7 @@ module Ci has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id + has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id Ci::JobArtifact.file_types.each do |key, value| has_one :"job_artifacts_#{key}", -> { where(file_type: value) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index b34fd3f1ec9..5e52062ef40 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -52,9 +52,15 @@ module Ci has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id' has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id' + has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_pipeline_id + has_one :source_pipeline, class_name: 'Ci::Sources::Pipeline', inverse_of: :pipeline has_one :chat_data, class_name: 'Ci::PipelineChatData' + has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline + has_one :triggered_by_pipeline, through: :source_pipeline, source: :source_pipeline + has_one :source_job, through: :source_pipeline, source: :source_job + accepts_nested_attributes_for :variables, reject_if: :persisted? delegate :id, to: :project, prefix: true diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb index cb92aef4bda..859abc4a0d5 100644 --- a/app/models/ci/pipeline_enums.rb +++ b/app/models/ci/pipeline_enums.rb @@ -22,6 +22,7 @@ module Ci schedule: 4, api: 5, external: 6, + pipeline: 7, chat: 8, merge_request_event: 10, external_pull_request_event: 11 diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb new file mode 100644 index 00000000000..feaec27281c --- /dev/null +++ b/app/models/ci/sources/pipeline.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Ci + module Sources + class Pipeline < ApplicationRecord + self.table_name = "ci_sources_pipelines" + + belongs_to :project, class_name: "Project" + belongs_to :pipeline, class_name: "Ci::Pipeline", inverse_of: :source_pipeline + + belongs_to :source_project, class_name: "Project", foreign_key: :source_project_id + belongs_to :source_job, class_name: "CommitStatus", foreign_key: :source_job_id + belongs_to :source_pipeline, class_name: "Ci::Pipeline", foreign_key: :source_pipeline_id + + validates :project, presence: true + validates :pipeline, presence: true + + validates :source_project, presence: true + validates :source_job, presence: true + validates :source_pipeline, presence: true + end + end +end + +::Ci::Sources::Pipeline.prepend_if_ee('::EE::Ci::Sources::Pipeline') diff --git a/app/models/group.rb b/app/models/group.rb index 0501fe94440..8b21206fccf 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -259,6 +259,10 @@ class Group < Namespace members_with_parents.maintainers.exists?(user_id: user) end + def has_container_repositories? + container_repositories.exists? + end + # @deprecated alias_method :has_master?, :has_maintainer? diff --git a/app/models/project.rb b/app/models/project.rb index d7e3dc676ca..4d518862146 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -297,6 +297,9 @@ class Project < ApplicationRecord has_many :external_pull_requests, inverse_of: :project + has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id + has_many :source_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :project_id + has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project accepts_nested_attributes_for :variables, allow_destroy: true diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb index 808e87c3fcf..71589ac8315 100644 --- a/app/serializers/pipeline_details_entity.rb +++ b/app/serializers/pipeline_details_entity.rb @@ -10,6 +10,7 @@ class PipelineDetailsEntity < PipelineEntity expose :manual_actions, using: BuildActionEntity expose :scheduled_actions, using: BuildActionEntity end -end -PipelineDetailsEntity.prepend_if_ee('EE::PipelineDetailsEntity') + expose :triggered_by_pipeline, as: :triggered_by, with: TriggeredPipelineEntity + expose :triggered_pipelines, as: :triggered, using: TriggeredPipelineEntity +end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index eaaeaf040a2..fc3160e3c69 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -54,9 +54,9 @@ class PipelineSerializer < BaseSerializer artifacts: { project: [:route, { namespace: :route }] } - } + }, + { triggered_by_pipeline: [:project, :user] }, + { triggered_pipelines: [:project, :user] } ] end end - -PipelineSerializer.prepend_if_ee('EE::PipelineSerializer') diff --git a/app/serializers/triggered_pipeline_entity.rb b/app/serializers/triggered_pipeline_entity.rb new file mode 100644 index 00000000000..fd7e4454abf --- /dev/null +++ b/app/serializers/triggered_pipeline_entity.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class TriggeredPipelineEntity < Grape::Entity + include RequestAwareEntity + + MAX_EXPAND_DEPTH = 3 + + expose :id + expose :user, using: UserEntity + expose :active?, as: :active + expose :coverage + expose :source + + expose :path do |pipeline| + project_pipeline_path(pipeline.project, pipeline) + end + + expose :details do + expose :detailed_status, as: :status, with: DetailedStatusEntity + + expose :ordered_stages, + as: :stages, using: StageEntity, + if: -> (_, opts) { can_read_details? && expand?(opts) } + end + + expose :triggered_by_pipeline, + as: :triggered_by, with: TriggeredPipelineEntity, + if: -> (_, opts) { can_read_details? && expand_for_path?(opts) } + + expose :triggered_pipelines, + as: :triggered, using: TriggeredPipelineEntity, + if: -> (_, opts) { can_read_details? && expand_for_path?(opts) } + + expose :project, using: ProjectEntity + + private + + alias_method :pipeline, :object + + def can_read_details? + can?(request.current_user, :read_pipeline, pipeline) + end + + def detailed_status + pipeline.detailed_status(request.current_user) + end + + def expand?(opts) + opts[:expanded].to_a.include?(pipeline.id) + end + + def expand_for_path?(opts) + # The `opts[:attr_path]` holds a list of all `exposes` in path + # The check ensures that we always expand only `triggered_by`, `triggered_by`, ... + # but not the `triggered_by`, `triggered` which would result in dead loop + attr_path = opts[:attr_path] + current_expose = attr_path.last + + # We expand at most to depth of MAX_DEPTH + # We ensure that we expand in one direction: triggered_by,... or triggered, ... + attr_path.length < MAX_EXPAND_DEPTH && + attr_path.all?(current_expose) && + expand?(opts) + end +end diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb index 0e99f142492..37b9b4c362c 100644 --- a/app/services/ci/pipeline_trigger_service.rb +++ b/app/services/ci/pipeline_trigger_service.rb @@ -38,11 +38,34 @@ module Ci end def create_pipeline_from_job(job) - # overridden in EE + # this check is to not leak the presence of the project if user cannot read it + return unless can?(job.user, :read_project, project) + + return error("400 Job has to be running", 400) unless job.running? + + pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref]) + .execute(:pipeline, ignore_skip_ci: true) do |pipeline| + source = job.sourced_pipelines.build( + source_pipeline: job.pipeline, + source_project: job.project, + pipeline: pipeline, + project: project) + + pipeline.source_pipeline = source + pipeline.variables.build(variables) + end + + if pipeline.persisted? + success(pipeline: pipeline) + else + error(pipeline.errors.messages, 400) + end end def job_from_token - # overridden in EE + strong_memoize(:job) do + Ci::Build.find_by_token(params[:token].to_s) + end end def variables @@ -52,5 +75,3 @@ module Ci end end end - -Ci::PipelineTriggerService.prepend_if_ee('EE::Ci::PipelineTriggerService') diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb index fe7e07ef9f0..6902b7bd529 100644 --- a/app/services/groups/transfer_service.rb +++ b/app/services/groups/transfer_service.rb @@ -7,7 +7,8 @@ module Groups namespace_with_same_path: s_('TransferGroup|The parent group already has a subgroup with the same path.'), group_is_already_root: s_('TransferGroup|Group is already a root group.'), same_parent_as_current: s_('TransferGroup|Group is already associated to the parent group.'), - invalid_policies: s_("TransferGroup|You don't have enough permissions.") + invalid_policies: s_("TransferGroup|You don't have enough permissions."), + group_contains_images: s_('TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.') }.freeze TransferError = Class.new(StandardError) @@ -46,6 +47,7 @@ module Groups raise_transfer_error(:same_parent_as_current) if same_parent? raise_transfer_error(:invalid_policies) unless valid_policies? raise_transfer_error(:namespace_with_same_path) if namespace_with_same_path? + raise_transfer_error(:group_contains_images) if group_projects_contain_registry_images? end def group_is_already_root? @@ -72,6 +74,10 @@ module Groups end # rubocop: enable CodeReuse/ActiveRecord + def group_projects_contain_registry_images? + @group.has_container_repositories? + end + def update_group_attributes if @new_parent_group && @new_parent_group.visibility_level < @group.visibility_level update_children_and_projects_visibility diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 534de601e20..be7502a193e 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -8,6 +8,11 @@ module Groups reject_parent_id! remove_unallowed_params + if renaming_group_with_container_registry_images? + group.errors.add(:base, container_images_error) + return false + end + return false unless valid_visibility_level_change?(group, params[:visibility_level]) return false unless valid_share_with_group_lock_change? @@ -35,6 +40,17 @@ module Groups # overridden in EE end + def renaming_group_with_container_registry_images? + new_path = params[:path] + + new_path && new_path != group.path && + group.has_container_repositories? + end + + def container_images_error + s_("GroupSettings|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.") + end + def after_update if group.previous_changes.include?(:visibility_level) && group.private? # don't enqueue immediately to prevent todos removal in case of a mistake diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index a55b7fc530a..c8b2adcf084 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -5,174 +5,178 @@ - signed_in = !!issuable_sidebar.dig(:current_user, :id) - can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit) -%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } - .issuable-sidebar - .block.issuable-sidebar-header - - if signed_in - %span.issuable-header-text.hide-collapsed.float-left - = _('To Do') - %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } } - = sidebar_gutter_toggle_icon - - if signed_in - = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar - - = form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f| - - if signed_in - .block.todo.hide-expanded - = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar, is_collapsed: true - .block.assignee.qa-assignee-block - = render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees - - = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar - - - milestone = issuable_sidebar[:milestone] || {} - .block.milestone - .sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } - = icon('clock-o', 'aria-hidden': 'true') - %span.milestone-title.collapse-truncated-title - - if milestone.present? - = milestone[:title] - - else - = _('None') - .title.hide-collapsed - = _('Milestone') - = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - - if can_edit_issuable - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" } - .value.hide-collapsed - - if milestone.present? - = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link' } - - else - %span.no-value - = _('None') - - .selectbox.hide-collapsed - = f.hidden_field 'milestone_id', value: milestone[:id], id: nil - = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], milestones: issuable_sidebar[:project_milestones_path], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }}) - - #issuable-time-tracker.block - // Fallback while content is loading - .title.hide-collapsed - = _('Time tracking') - = icon('spinner spin', 'aria-hidden': 'true') - - - if issuable_sidebar.has_key?(:due_date) - .block.due_date - .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) } - = icon('calendar', 'aria-hidden': 'true') - %span.js-due-date-sidebar-value - = issuable_sidebar[:due_date].try(:to_s, :medium) || 'None' +- if Feature.enabled?(:vue_issuable_sidebar, @project.group) + %aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in, + sidebar_status_class: sidebar_gutter_collapsed_class } } +- else + %aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } + .issuable-sidebar + .block.issuable-sidebar-header + - if signed_in + %span.issuable-header-text.hide-collapsed.float-left + = _('To Do') + %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } } + = sidebar_gutter_toggle_icon + - if signed_in + = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar + + = form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f| + - if signed_in + .block.todo.hide-expanded + = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar, is_collapsed: true + .block.assignee.qa-assignee-block + = render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees + + = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar + + - milestone = issuable_sidebar[:milestone] || {} + .block.milestone + .sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } + = icon('clock-o', 'aria-hidden': 'true') + %span.milestone-title.collapse-truncated-title + - if milestone.present? + = milestone[:title] + - else + = _('None') .title.hide-collapsed - = _('Due date') + = _('Milestone') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" } + = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" } .value.hide-collapsed - %span.value-content - - if issuable_sidebar[:due_date] - %span.bold= issuable_sidebar[:due_date].to_s(:medium) - - else - %span.no-value - = _('None') + - if milestone.present? + = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link' } + - else + %span.no-value + = _('None') + + .selectbox.hide-collapsed + = f.hidden_field 'milestone_id', value: milestone[:id], id: nil + = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], milestones: issuable_sidebar[:project_milestones_path], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }}) + + #issuable-time-tracker.block + // Fallback while content is loading + .title.hide-collapsed + = _('Time tracking') + = icon('spinner spin', 'aria-hidden': 'true') + + - if issuable_sidebar.has_key?(:due_date) + .block.due_date + .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) } + = icon('calendar', 'aria-hidden': 'true') + %span.js-due-date-sidebar-value + = issuable_sidebar[:due_date].try(:to_s, :medium) || 'None' + .title.hide-collapsed + = _('Due date') + = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') + - if can_edit_issuable + = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" } + .value.hide-collapsed + %span.value-content + - if issuable_sidebar[:due_date] + %span.bold= issuable_sidebar[:due_date].to_s(:medium) + - else + %span.no-value + = _('None') + - if can_edit_issuable + %span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable_sidebar[:due_date].nil?) } + \- + %a.js-remove-due-date{ href: "#", role: "button" } + = _('remove due date') - if can_edit_issuable - %span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable_sidebar[:due_date].nil?) } - \- - %a.js-remove-due-date{ href: "#", role: "button" } - = _('remove due date') - - if can_edit_issuable - .selectbox.hide-collapsed - = f.hidden_field :due_date, value: issuable_sidebar[:due_date].try(:strftime, 'yy-mm-dd') - .dropdown - %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable_type}[due_date]", ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], display: 'static' } } - %span.dropdown-toggle-text - = _('Due date') - = icon('chevron-down', 'aria-hidden': 'true') - .dropdown-menu.dropdown-menu-due-date - = dropdown_title(_('Due date')) - = dropdown_content do - .js-due-date-calendar - - - selected_labels = issuable_sidebar[:labels] - .block.labels - .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } } - = icon('tags', 'aria-hidden': 'true') - %span - = selected_labels.size - .title.hide-collapsed - = _('Labels') - = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - - if can_edit_issuable - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right', data: { track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" } - .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) } - - if selected_labels.any? - - selected_labels.each do |label_hash| - = render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title])) - - else - %span.no-value - = _('None') - .selectbox.hide-collapsed - - selected_labels.each do |label| - = hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil - .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: sidebar_label_dropdown_data(issuable_type, issuable_sidebar) } - %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } - = multi_label_name(selected_labels, "Labels") - = icon('chevron-down', 'aria-hidden': 'true') - .dropdown-menu.dropdown-select.dropdown-menu-paging.qa-dropdown-menu-labels.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height - = render partial: "shared/issuable/label_page_default" - - if issuable_sidebar.dig(:current_user, :can_admin_label) - = render partial: "shared/issuable/label_page_create" - - = render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar - - - if issuable_sidebar.has_key?(:confidential) + .selectbox.hide-collapsed + = f.hidden_field :due_date, value: issuable_sidebar[:due_date].try(:strftime, 'yy-mm-dd') + .dropdown + %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable_type}[due_date]", ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], display: 'static' } } + %span.dropdown-toggle-text + = _('Due date') + = icon('chevron-down', 'aria-hidden': 'true') + .dropdown-menu.dropdown-menu-due-date + = dropdown_title(_('Due date')) + = dropdown_content do + .js-due-date-calendar + + - selected_labels = issuable_sidebar[:labels] + .block.labels + .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } } + = icon('tags', 'aria-hidden': 'true') + %span + = selected_labels.size + .title.hide-collapsed + = _('Labels') + = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') + - if can_edit_issuable + = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right', data: { track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" } + .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) } + - if selected_labels.any? + - selected_labels.each do |label_hash| + = render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title])) + - else + %span.no-value + = _('None') + .selectbox.hide-collapsed + - selected_labels.each do |label| + = hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil + .dropdown + %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: sidebar_label_dropdown_data(issuable_type, issuable_sidebar) } + %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } + = multi_label_name(selected_labels, "Labels") + = icon('chevron-down', 'aria-hidden': 'true') + .dropdown-menu.dropdown-select.dropdown-menu-paging.qa-dropdown-menu-labels.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height + = render partial: "shared/issuable/label_page_default" + - if issuable_sidebar.dig(:current_user, :can_admin_label) + = render partial: "shared/issuable/label_page_create" + + = render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar + + - if issuable_sidebar.has_key?(:confidential) + -# haml-lint:disable InlineJavaScript + %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe + #js-confidential-entry-point + -# haml-lint:disable InlineJavaScript - %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe - #js-confidential-entry-point + %script#js-lock-issue-data{ type: "application/json" }= { is_locked: !!issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe + #js-lock-entry-point + + .js-sidebar-participants-entry-point + + - if signed_in + - if issuable_sidebar[:project_emails_disabled] + .block.js-emails-disabled + .sidebar-collapsed-icon.has-tooltip{ title: notification_description(:owner_disabled), data: { placement: "left", container: "body", boundary: 'viewport' } } + = notification_setting_icon + .hide-collapsed= notification_description(:owner_disabled) + - else + .js-sidebar-subscriptions-entry-point + + - project_ref = issuable_sidebar[:reference] + .block.project-reference + .sidebar-collapsed-icon.dont-change-state + = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport') + .cross-project-reference.hide-collapsed + %span + = _('Reference:') + %cite{ title: project_ref } + = project_ref + = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport') + + - if issuable_sidebar.dig(:current_user, :can_move) + .block.js-sidebar-move-issue-block + .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') } + = custom_icon('icon_arrow_right') + .dropdown.sidebar-move-issue-dropdown.hide-collapsed + %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button', + data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_event: "click_button", track_value: "" } } + = _('Move issue') + .dropdown-menu.dropdown-menu-selectable.dropdown-extended-height + = dropdown_title(_('Move issue')) + = dropdown_filter(_('Search project'), search_id: 'sidebar-move-issue-dropdown-search') + = dropdown_content + = dropdown_loading + = dropdown_footer add_content_class: true do + %button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ type: 'button', disabled: true } + = _('Move') + = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon') -# haml-lint:disable InlineJavaScript - %script#js-lock-issue-data{ type: "application/json" }= { is_locked: !!issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe - #js-lock-entry-point - - .js-sidebar-participants-entry-point - - - if signed_in - - if issuable_sidebar[:project_emails_disabled] - .block.js-emails-disabled - .sidebar-collapsed-icon.has-tooltip{ title: notification_description(:owner_disabled), data: { placement: "left", container: "body", boundary: 'viewport' } } - = notification_setting_icon - .hide-collapsed= notification_description(:owner_disabled) - - else - .js-sidebar-subscriptions-entry-point - - - project_ref = issuable_sidebar[:reference] - .block.project-reference - .sidebar-collapsed-icon.dont-change-state - = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport') - .cross-project-reference.hide-collapsed - %span - = _('Reference:') - %cite{ title: project_ref } - = project_ref - = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport') - - - if issuable_sidebar.dig(:current_user, :can_move) - .block.js-sidebar-move-issue-block - .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') } - = custom_icon('icon_arrow_right') - .dropdown.sidebar-move-issue-dropdown.hide-collapsed - %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button', - data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_event: "click_button", track_value: "" } } - = _('Move issue') - .dropdown-menu.dropdown-menu-selectable.dropdown-extended-height - = dropdown_title(_('Move issue')) - = dropdown_filter(_('Search project'), search_id: 'sidebar-move-issue-dropdown-search') - = dropdown_content - = dropdown_loading - = dropdown_footer add_content_class: true do - %button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ type: 'button', disabled: true } - = _('Move') - = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon') - - -# haml-lint:disable InlineJavaScript - %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable_sidebar).to_json.html_safe + %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable_sidebar).to_json.html_safe diff --git a/changelogs/unreleased/28243-check-for-docker-images-before-renaming-group.yml b/changelogs/unreleased/28243-check-for-docker-images-before-renaming-group.yml new file mode 100644 index 00000000000..8717d59b1bc --- /dev/null +++ b/changelogs/unreleased/28243-check-for-docker-images-before-renaming-group.yml @@ -0,0 +1,6 @@ +--- +title: Prevents a group path change when a project inside the group has container + registry images +merge_request: 17583 +author: +type: fixed diff --git a/changelogs/unreleased/31573-cross-project-pipeline-triggering-does-not-work-in-core.yml b/changelogs/unreleased/31573-cross-project-pipeline-triggering-does-not-work-in-core.yml new file mode 100644 index 00000000000..1638746ea72 --- /dev/null +++ b/changelogs/unreleased/31573-cross-project-pipeline-triggering-does-not-work-in-core.yml @@ -0,0 +1,5 @@ +--- +title: Allow cross-project pipeline triggering with CI_JOB_TOKEN in core +merge_request: 17251 +author: +type: added diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb index 43c159fee27..8a253893892 100644 --- a/lib/gitlab/daemon.rb +++ b/lib/gitlab/daemon.rb @@ -34,7 +34,9 @@ module Gitlab @mutex.synchronize do break thread if thread? - @thread = Thread.new { start_working } + if start_working + @thread = Thread.new { run_thread } + end end end @@ -57,10 +59,18 @@ module Gitlab private + # Executed in lock context before starting thread + # Needs to return success def start_working + true + end + + # Executed in separate thread + def run_thread raise NotImplementedError end + # Executed in lock context def stop_working # no-ops end diff --git a/lib/gitlab/danger/request_helper.rb b/lib/gitlab/danger/request_helper.rb new file mode 100644 index 00000000000..06da4ed9ad3 --- /dev/null +++ b/lib/gitlab/danger/request_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'net/http' +require 'json' + +module Gitlab + module Danger + module RequestHelper + HTTPError = Class.new(RuntimeError) + + # @param [String] url + def self.http_get_json(url) + rsp = Net::HTTP.get_response(URI.parse(url)) + + unless rsp.is_a?(Net::HTTPOK) + raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}" + end + + JSON.parse(rsp.body) + end + end + end +end diff --git a/lib/gitlab/danger/roulette.rb b/lib/gitlab/danger/roulette.rb index 25de0a87c9d..0700a72c918 100644 --- a/lib/gitlab/danger/roulette.rb +++ b/lib/gitlab/danger/roulette.rb @@ -1,16 +1,11 @@ # frozen_string_literal: true -require 'net/http' -require 'json' -require 'cgi' - require_relative 'teammate' module Gitlab module Danger module Roulette ROULETTE_DATA_URL = 'https://about.gitlab.com/roulette.json' - HTTPError = Class.new(RuntimeError) # Looks up the current list of GitLab team members and parses it into a # useful form @@ -19,7 +14,7 @@ module Gitlab def team @team ||= begin - data = http_get_json(ROULETTE_DATA_URL) + data = Gitlab::Danger::RequestHelper.http_get_json(ROULETTE_DATA_URL) data.map { |hash| ::Gitlab::Danger::Teammate.new(hash) } rescue JSON::ParserError raise "Failed to parse JSON response from #{ROULETTE_DATA_URL}" @@ -44,6 +39,7 @@ module Gitlab # Known issue: If someone is rejected due to OOO, and then becomes not OOO, the # selection will change on next spin + # @param [Array<Teammate>] people def spin_for_person(people, random:) people.shuffle(random: random) .find(&method(:valid_person?)) @@ -51,32 +47,17 @@ module Gitlab private + # @param [Teammate] person + # @return [Boolean] def valid_person?(person) - !mr_author?(person) && !out_of_office?(person) + !mr_author?(person) && !person.out_of_office? end + # @param [Teammate] person + # @return [Boolean] def mr_author?(person) person.username == gitlab.mr_author end - - def out_of_office?(person) - username = CGI.escape(person.username) - api_endpoint = "https://gitlab.com/api/v4/users/#{username}/status" - response = http_get_json(api_endpoint) - response["message"]&.match?(/OOO/i) - rescue HTTPError, JSON::ParserError - false # this is no worse than not checking for OOO - end - - def http_get_json(url) - rsp = Net::HTTP.get_response(URI.parse(url)) - - unless rsp.is_a?(Net::HTTPSuccess) - raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}" - end - - JSON.parse(rsp.body) - end end end end diff --git a/lib/gitlab/danger/teammate.rb b/lib/gitlab/danger/teammate.rb index 4ad66f61c2b..c0a2d909f69 100644 --- a/lib/gitlab/danger/teammate.rb +++ b/lib/gitlab/danger/teammate.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'cgi' + module Gitlab module Danger class Teammate @@ -34,6 +36,18 @@ module Gitlab has_capability?(project, category, :maintainer, labels) end + def status + api_endpoint = "https://gitlab.com/api/v4/users/#{CGI.escape(username)}/status" + @status ||= Gitlab::Danger::RequestHelper.http_get_json(api_endpoint) + rescue Gitlab::Danger::RequestHelper::HTTPError, JSON::ParserError + nil # better no status than a crashing Danger + end + + # @return [Boolean] + def out_of_office? + status&.dig("message")&.match?(/OOO/i) || false + end + private def has_capability?(project, category, kind, labels) diff --git a/lib/gitlab/gitaly_client/storage_service.rb b/lib/gitlab/gitaly_client/storage_service.rb deleted file mode 100644 index e69de29bb2d..00000000000 --- a/lib/gitlab/gitaly_client/storage_service.rb +++ /dev/null diff --git a/lib/gitlab/metrics/exporter/base_exporter.rb b/lib/gitlab/metrics/exporter/base_exporter.rb index b56770e224b..3aac18f59c6 100644 --- a/lib/gitlab/metrics/exporter/base_exporter.rb +++ b/lib/gitlab/metrics/exporter/base_exporter.rb @@ -40,7 +40,14 @@ module Gitlab ::Gitlab::HealthChecks::Probes::Liveness.new, req, res) end server.mount '/', Rack::Handler::WEBrick, rack_app - server.start + + true + end + + def run_thread + server&.start + rescue IOError + # ignore forcibily closed servers end def stop_working diff --git a/lib/gitlab/metrics/samplers/base_sampler.rb b/lib/gitlab/metrics/samplers/base_sampler.rb index d7d848d2833..90051f85f31 100644 --- a/lib/gitlab/metrics/samplers/base_sampler.rb +++ b/lib/gitlab/metrics/samplers/base_sampler.rb @@ -50,6 +50,11 @@ module Gitlab def start_working @running = true + + true + end + + def run_thread sleep(sleep_interval) while running safe_sample diff --git a/lib/gitlab/sidekiq_daemon/memory_killer.rb b/lib/gitlab/sidekiq_daemon/memory_killer.rb index eb58435e3f1..804170169e8 100644 --- a/lib/gitlab/sidekiq_daemon/memory_killer.rb +++ b/lib/gitlab/sidekiq_daemon/memory_killer.rb @@ -29,7 +29,7 @@ module Gitlab private - def start_working + def run_thread Sidekiq.logger.info( class: self.class.to_s, action: 'start', diff --git a/lib/gitlab/sidekiq_daemon/monitor.rb b/lib/gitlab/sidekiq_daemon/monitor.rb index 09f30837cd2..a3d61c69ae1 100644 --- a/lib/gitlab/sidekiq_daemon/monitor.rb +++ b/lib/gitlab/sidekiq_daemon/monitor.rb @@ -61,7 +61,7 @@ module Gitlab private - def start_working + def run_thread return unless notification_channel_enabled? begin diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 237b2b7cb0d..0741e0445a6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8282,6 +8282,9 @@ msgstr "" msgid "GroupSettings|Be careful. Changing a group's parent can have unintended %{side_effects_link_start}side effects%{side_effects_link_end}." msgstr "" +msgid "GroupSettings|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again." +msgstr "" + msgid "GroupSettings|Change group path" msgstr "" @@ -17290,6 +17293,9 @@ msgstr "" msgid "Transfer project" msgstr "" +msgid "TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again." +msgstr "" + msgid "TransferGroup|Database is not supported." msgstr "" diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index a35ef99ef12..3c39a6468e5 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -385,6 +385,29 @@ describe GroupsController do expect(response).to have_gitlab_http_status(302) expect(group.reload.project_creation_level).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) end + + context 'when a project inside the group has container repositories' do + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(repository: /image/, tags: %w[rc1]) + create(:container_repository, project: project, name: :image) + end + + it 'does allow the group to be renamed' do + post :update, params: { id: group.to_param, group: { name: 'new_name' } } + + expect(controller).to set_flash[:notice] + expect(response).to have_gitlab_http_status(302) + expect(group.reload.name).to eq('new_name') + end + + it 'does not allow to path of the group to be changed' do + post :update, params: { id: group.to_param, group: { path: 'new_path' } } + + expect(assigns(:group).errors[:base].first).to match(/Docker images in their Container Registry/) + expect(response).to have_gitlab_http_status(200) + end + end end describe '#ensure_canonical_path' do @@ -673,6 +696,28 @@ describe GroupsController do expect(response).to have_gitlab_http_status(404) end end + + context 'transferring when a project has container images' do + let(:group) { create(:group, :public, :nested) } + let!(:group_member) { create(:group_member, :owner, group: group, user: user) } + + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(repository: /image/, tags: %w[rc1]) + create(:container_repository, project: project, name: :image) + + put :transfer, + params: { + id: group.to_param, + new_parent_group_id: '' + } + end + + it 'does not allow the group to be transferred' do + expect(controller).to set_flash[:alert].to match(/Docker images in their Container Registry/) + expect(response).to redirect_to(edit_group_path(group)) + end + end end context 'token authentication' do diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index d964b0672a9..e3ad36f8d24 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -217,6 +217,193 @@ describe Projects::PipelinesController do end end + context 'with triggered pipelines' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:source_project) { create(:project, :repository) } + let_it_be(:target_project) { create(:project, :repository) } + let_it_be(:root_pipeline) { create_pipeline(project) } + let_it_be(:source_pipeline) { create_pipeline(source_project) } + let_it_be(:source_of_source_pipeline) { create_pipeline(source_project) } + let_it_be(:target_pipeline) { create_pipeline(target_project) } + let_it_be(:target_of_target_pipeline) { create_pipeline(target_project) } + + before do + create_link(source_of_source_pipeline, source_pipeline) + create_link(source_pipeline, root_pipeline) + create_link(root_pipeline, target_pipeline) + create_link(target_pipeline, target_of_target_pipeline) + end + + shared_examples 'not expanded' do + let(:expected_stages) { be_nil } + + it 'does return base details' do + get_pipeline_json(root_pipeline) + + expect(json_response['triggered_by']).to include('id' => source_pipeline.id) + expect(json_response['triggered']).to contain_exactly( + include('id' => target_pipeline.id)) + end + + it 'does not expand triggered_by pipeline' do + get_pipeline_json(root_pipeline) + + triggered_by = json_response['triggered_by'] + expect(triggered_by['triggered_by']).to be_nil + expect(triggered_by['triggered']).to be_nil + expect(triggered_by['details']['stages']).to expected_stages + end + + it 'does not expand triggered pipelines' do + get_pipeline_json(root_pipeline) + + first_triggered = json_response['triggered'].first + expect(first_triggered['triggered_by']).to be_nil + expect(first_triggered['triggered']).to be_nil + expect(first_triggered['details']['stages']).to expected_stages + end + end + + shared_examples 'expanded' do + it 'does return base details' do + get_pipeline_json(root_pipeline) + + expect(json_response['triggered_by']).to include('id' => source_pipeline.id) + expect(json_response['triggered']).to contain_exactly( + include('id' => target_pipeline.id)) + end + + it 'does expand triggered_by pipeline' do + get_pipeline_json(root_pipeline) + + triggered_by = json_response['triggered_by'] + expect(triggered_by['triggered_by']).to include( + 'id' => source_of_source_pipeline.id) + expect(triggered_by['details']['stages']).not_to be_nil + end + + it 'does not recursively expand triggered_by' do + get_pipeline_json(root_pipeline) + + triggered_by = json_response['triggered_by'] + expect(triggered_by['triggered']).to be_nil + end + + it 'does expand triggered pipelines' do + get_pipeline_json(root_pipeline) + + first_triggered = json_response['triggered'].first + expect(first_triggered['triggered']).to contain_exactly( + include('id' => target_of_target_pipeline.id)) + expect(first_triggered['details']['stages']).not_to be_nil + end + + it 'does not recursively expand triggered' do + get_pipeline_json(root_pipeline) + + first_triggered = json_response['triggered'].first + expect(first_triggered['triggered_by']).to be_nil + end + end + + context 'when it does have permission to read other projects' do + before do + source_project.add_developer(user) + target_project.add_developer(user) + end + + context 'when not-expanding any pipelines' do + let(:expanded) { nil } + + it_behaves_like 'not expanded' + end + + context 'when expanding non-existing pipeline' do + let(:expanded) { [-1] } + + it_behaves_like 'not expanded' + end + + context 'when expanding pipeline that is not directly expandable' do + let(:expanded) { [source_of_source_pipeline.id, target_of_target_pipeline.id] } + + it_behaves_like 'not expanded' + end + + context 'when expanding self' do + let(:expanded) { [root_pipeline.id] } + + context 'it does not recursively expand pipelines' do + it_behaves_like 'not expanded' + end + end + + context 'when expanding source and target pipeline' do + let(:expanded) { [source_pipeline.id, target_pipeline.id] } + + it_behaves_like 'expanded' + + context 'when expand depth is limited to 1' do + before do + stub_const('TriggeredPipelineEntity::MAX_EXPAND_DEPTH', 1) + end + + it_behaves_like 'not expanded' do + # We expect that triggered/triggered_by is not expanded, + # but we still return details.stages for that pipeline + let(:expected_stages) { be_a(Array) } + end + end + end + + context 'when expanding all' do + let(:expanded) do + [ + source_of_source_pipeline.id, + source_pipeline.id, + root_pipeline.id, + target_pipeline.id, + target_of_target_pipeline.id + ] + end + + it_behaves_like 'expanded' + end + end + + context 'when does not have permission to read other projects' do + let(:expanded) { [source_pipeline.id, target_pipeline.id] } + + it_behaves_like 'not expanded' + end + + def create_pipeline(project) + create(:ci_empty_pipeline, project: project).tap do |pipeline| + create(:ci_build, pipeline: pipeline, stage: 'test', name: 'rspec') + end + end + + def create_link(source_pipeline, pipeline) + source_pipeline.sourced_pipelines.create!( + source_job: source_pipeline.builds.all.sample, + source_project: source_pipeline.project, + project: pipeline.project, + pipeline: pipeline + ) + end + + def get_pipeline_json(pipeline) + params = { + namespace_id: pipeline.project.namespace, + project_id: pipeline.project, + id: pipeline, + expanded: expanded + } + + get :show, params: params.compact, format: :json + end + end + def get_pipeline_json get :show, params: { namespace_id: project.namespace, project_id: project, id: pipeline }, format: :json end diff --git a/spec/dependencies/omniauth_saml_spec.rb b/spec/dependencies/omniauth_saml_spec.rb index ccc604dc230..8a685648c71 100644 --- a/spec/dependencies/omniauth_saml_spec.rb +++ b/spec/dependencies/omniauth_saml_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'omniauth/strategies/saml' diff --git a/spec/factories/ci/sources/pipelines.rb b/spec/factories/ci/sources/pipelines.rb new file mode 100644 index 00000000000..57495502944 --- /dev/null +++ b/spec/factories/ci/sources/pipelines.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :ci_sources_pipeline, class: Ci::Sources::Pipeline do + after(:build) do |source| + source.project ||= source.pipeline.project + source.source_pipeline ||= source.source_job.pipeline + source.source_project ||= source.source_pipeline.project + end + + source_job factory: :ci_build + + pipeline factory: :ci_empty_pipeline + end +end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 458c67760d5..f5d5bc7f5b9 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'tempfile' @@ -424,8 +426,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it 'loads job trace' do expect(page).to have_content 'BUILD TRACE' - job.trace.write('a+b') do |stream| - stream.append(' and more trace', 11) + job.trace.write(+'a+b') do |stream| + stream.append(+' and more trace', 11) end expect(page).to have_content 'BUILD TRACE and more trace' diff --git a/spec/finders/access_requests_finder_spec.rb b/spec/finders/access_requests_finder_spec.rb index 605777462bb..4c67087b50f 100644 --- a/spec/finders/access_requests_finder_spec.rb +++ b/spec/finders/access_requests_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe AccessRequestsFinder do diff --git a/spec/finders/admin/projects_finder_spec.rb b/spec/finders/admin/projects_finder_spec.rb index 44cc8debd04..eb5d0bba183 100644 --- a/spec/finders/admin/projects_finder_spec.rb +++ b/spec/finders/admin/projects_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Admin::ProjectsFinder do diff --git a/spec/finders/autocomplete/move_to_project_finder_spec.rb b/spec/finders/autocomplete/move_to_project_finder_spec.rb index 4a87b47bd08..f997dd32c40 100644 --- a/spec/finders/autocomplete/move_to_project_finder_spec.rb +++ b/spec/finders/autocomplete/move_to_project_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Autocomplete::MoveToProjectFinder do diff --git a/spec/finders/autocomplete/users_finder_spec.rb b/spec/finders/autocomplete/users_finder_spec.rb index f3b54ca0461..5d340c46114 100644 --- a/spec/finders/autocomplete/users_finder_spec.rb +++ b/spec/finders/autocomplete/users_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Autocomplete::UsersFinder do diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb index 3fc86f3e408..1a33bdf11d7 100644 --- a/spec/finders/branches_finder_spec.rb +++ b/spec/finders/branches_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe BranchesFinder do diff --git a/spec/finders/clusters_finder_spec.rb b/spec/finders/clusters_finder_spec.rb index da529e0670f..f6ea8347f67 100644 --- a/spec/finders/clusters_finder_spec.rb +++ b/spec/finders/clusters_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ClustersFinder do diff --git a/spec/finders/concerns/finder_methods_spec.rb b/spec/finders/concerns/finder_methods_spec.rb index e074e53c2c5..2e44df8b044 100644 --- a/spec/finders/concerns/finder_methods_spec.rb +++ b/spec/finders/concerns/finder_methods_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe FinderMethods do diff --git a/spec/finders/concerns/finder_with_cross_project_access_spec.rb b/spec/finders/concerns/finder_with_cross_project_access_spec.rb index f29acb521a8..7f6190f96e0 100644 --- a/spec/finders/concerns/finder_with_cross_project_access_spec.rb +++ b/spec/finders/concerns/finder_with_cross_project_access_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe FinderWithCrossProjectAccess do diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb index ee84fd067d4..1d907261fe9 100644 --- a/spec/finders/contributed_projects_finder_spec.rb +++ b/spec/finders/contributed_projects_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ContributedProjectsFinder do diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb index 25835bb4d94..69687eaa99f 100644 --- a/spec/finders/environments_finder_spec.rb +++ b/spec/finders/environments_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe EnvironmentsFinder do diff --git a/spec/finders/events_finder_spec.rb b/spec/finders/events_finder_spec.rb index 3bce46cc4d1..848030262cd 100644 --- a/spec/finders/events_finder_spec.rb +++ b/spec/finders/events_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe EventsFinder do diff --git a/spec/finders/fork_projects_finder_spec.rb b/spec/finders/fork_projects_finder_spec.rb index 98cff37205e..2fba53a74a0 100644 --- a/spec/finders/fork_projects_finder_spec.rb +++ b/spec/finders/fork_projects_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ForkProjectsFinder do diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb index 5fb6739d6e2..17875a9b9ab 100644 --- a/spec/finders/group_descendants_finder_spec.rb +++ b/spec/finders/group_descendants_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe GroupDescendantsFinder do diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb index 49b0e14241e..12f92f6b5b0 100644 --- a/spec/finders/group_members_finder_spec.rb +++ b/spec/finders/group_members_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe GroupMembersFinder, '#execute' do diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb index f4bd8a3f6ba..b291b5d4b90 100644 --- a/spec/finders/group_projects_finder_spec.rb +++ b/spec/finders/group_projects_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe GroupProjectsFinder do diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb index c8875d1f92d..741a89a270b 100644 --- a/spec/finders/groups_finder_spec.rb +++ b/spec/finders/groups_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe GroupsFinder do diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index a17ff1ad50d..c27ce263bf0 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe IssuesFinder do diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb index ae3e55f90f1..b01bd44470a 100644 --- a/spec/finders/joined_groups_finder_spec.rb +++ b/spec/finders/joined_groups_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe JoinedGroupsFinder do diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index ba41ded112a..2681f098fec 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe LabelsFinder do diff --git a/spec/finders/license_template_finder_spec.rb b/spec/finders/license_template_finder_spec.rb index f6f40bf33cc..183ee67d801 100644 --- a/spec/finders/license_template_finder_spec.rb +++ b/spec/finders/license_template_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe LicenseTemplateFinder do diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb index 5134db96133..9849808c255 100644 --- a/spec/finders/members_finder_spec.rb +++ b/spec/finders/members_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MembersFinder, '#execute' do diff --git a/spec/finders/merge_request_target_project_finder_spec.rb b/spec/finders/merge_request_target_project_finder_spec.rb index d26a75179de..1d78b7ba4e3 100644 --- a/spec/finders/merge_request_target_project_finder_spec.rb +++ b/spec/finders/merge_request_target_project_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MergeRequestTargetProjectFinder do diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 6c0bbeff4f4..a396284f1e9 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MergeRequestsFinder do diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb index 34c7b508c56..3545ff35ed8 100644 --- a/spec/finders/milestones_finder_spec.rb +++ b/spec/finders/milestones_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MilestonesFinder do diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 88906adfeeb..44636a22ef9 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe NotesFinder do diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb index 3e849c9a644..a44daf585ba 100644 --- a/spec/finders/personal_access_tokens_finder_spec.rb +++ b/spec/finders/personal_access_tokens_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe PersonalAccessTokensFinder do diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb index ef7dd0cd4a8..7686dd3dc9d 100644 --- a/spec/finders/personal_projects_finder_spec.rb +++ b/spec/finders/personal_projects_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe PersonalProjectsFinder do diff --git a/spec/finders/pipeline_schedules_finder_spec.rb b/spec/finders/pipeline_schedules_finder_spec.rb index 2fefa0280d1..8d0bde15e03 100644 --- a/spec/finders/pipeline_schedules_finder_spec.rb +++ b/spec/finders/pipeline_schedules_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe PipelineSchedulesFinder do diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb index b23fd8ccdc6..05d13a76e0e 100644 --- a/spec/finders/pipelines_finder_spec.rb +++ b/spec/finders/pipelines_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe PipelinesFinder do diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index ac866e49fcd..4ec12b5a7f7 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ProjectsFinder do diff --git a/spec/finders/runner_jobs_finder_spec.rb b/spec/finders/runner_jobs_finder_spec.rb index 01f45a37ba8..c11f9182036 100644 --- a/spec/finders/runner_jobs_finder_spec.rb +++ b/spec/finders/runner_jobs_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe RunnerJobsFinder do diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 72de05b5131..e7372189d17 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe SnippetsFinder do diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb index 460e278e2d3..85f970b71c4 100644 --- a/spec/finders/tags_finder_spec.rb +++ b/spec/finders/tags_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe TagsFinder do diff --git a/spec/finders/template_finder_spec.rb b/spec/finders/template_finder_spec.rb index 114af9461e0..ed47752cf60 100644 --- a/spec/finders/template_finder_spec.rb +++ b/spec/finders/template_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe TemplateFinder do diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb index 9a3ffffb3f2..5d284f4cf17 100644 --- a/spec/finders/todos_finder_spec.rb +++ b/spec/finders/todos_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe TodosFinder do diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb index 5ebceeb7586..eef6448a4a2 100644 --- a/spec/finders/user_recent_events_finder_spec.rb +++ b/spec/finders/user_recent_events_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe UserRecentEventsFinder do diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb index d71d3c99272..7f1fc1cc1c5 100644 --- a/spec/finders/users_finder_spec.rb +++ b/spec/finders/users_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe UsersFinder do diff --git a/spec/lib/gitlab/daemon_spec.rb b/spec/lib/gitlab/daemon_spec.rb index 0372b770844..cf1f089c577 100644 --- a/spec/lib/gitlab/daemon_spec.rb +++ b/spec/lib/gitlab/daemon_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Daemon do subject { described_class.new } before do - allow(subject).to receive(:start_working) + allow(subject).to receive(:run_thread) allow(subject).to receive(:stop_working) end @@ -44,7 +44,7 @@ describe Gitlab::Daemon do it 'starts the Daemon' do expect { subject.start.join }.to change { subject.thread? }.from(false).to(true) - expect(subject).to have_received(:start_working) + expect(subject).to have_received(:run_thread) end end @@ -52,7 +52,21 @@ describe Gitlab::Daemon do it "doesn't shutdown stopped Daemon" do expect { subject.stop }.not_to change { subject.thread? } - expect(subject).not_to have_received(:start_working) + expect(subject).not_to have_received(:run_thread) + end + end + end + + describe '#start_working' do + context 'when start_working fails' do + before do + expect(subject).to receive(:start_working) { false } + end + + it 'does not start thread' do + expect(subject).not_to receive(:run_thread) + + expect(subject.start).to eq(nil) end end end @@ -66,7 +80,7 @@ describe Gitlab::Daemon do it "doesn't start running Daemon" do expect { subject.start.join }.not_to change { subject.thread } - expect(subject).to have_received(:start_working).once + expect(subject).to have_received(:run_thread).once end end @@ -79,7 +93,7 @@ describe Gitlab::Daemon do context 'when stop_working raises exception' do before do - allow(subject).to receive(:start_working) do + allow(subject).to receive(:run_thread) do sleep(1000) end end @@ -108,7 +122,7 @@ describe Gitlab::Daemon do expect(subject.start).to be_nil expect { subject.start }.not_to change { subject.thread? } - expect(subject).not_to have_received(:start_working) + expect(subject).not_to have_received(:run_thread) end end diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb index ca036390bde..36486cbbc7d 100644 --- a/spec/lib/gitlab/danger/teammate_spec.rb +++ b/spec/lib/gitlab/danger/teammate_spec.rb @@ -2,11 +2,13 @@ require 'fast_spec_helper' +require 'rspec-parameterized' + require 'gitlab/danger/teammate' describe Gitlab::Danger::Teammate do - subject { described_class.new(options) } - let(:options) { { 'projects' => projects, 'role' => role } } + subject { described_class.new(options.stringify_keys) } + let(:options) { { username: 'luigi', projects: projects, role: role } } let(:projects) { { project => capabilities } } let(:role) { 'Engineer, Manage' } let(:labels) { [] } @@ -95,4 +97,64 @@ describe Gitlab::Danger::Teammate do expect(subject.maintainer?(project, :frontend, labels)).to be_falsey end end + + describe '#status' do + let(:capabilities) { ['dish washing'] } + + context 'with empty cache' do + context 'for successful request' do + it 'returns the response' do + mock_status = double(does_not: 'matter') + expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json) + .and_return(mock_status) + + expect(subject.status).to be mock_status + end + end + + context 'for failing request' do + it 'returns nil' do + expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json) + .and_raise(Gitlab::Danger::RequestHelper::HTTPError.new) + + expect(subject.status).to be nil + end + end + end + + context 'with filled cache' do + it 'returns the cached response' do + mock_status = double(does_not: 'matter') + expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json) + .and_return(mock_status) + subject.status + + expect(Gitlab::Danger::RequestHelper).not_to receive(:http_get_json) + expect(subject.status).to be mock_status + end + end + end + + describe '#out_of_office?' do + using RSpec::Parameterized::TableSyntax + + let(:capabilities) { ['dry head'] } + + where(:status, :result) do + nil | false + {} | false + { message: 'dear reader' } | false + { message: 'OOO: massage' } | true + { message: 'love it SOOO much' } | true + end + + with_them do + before do + expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json) + .and_return(status&.stringify_keys) + end + + it { expect(subject.out_of_office?).to be result } + end + end end diff --git a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb index 263cc821c1a..adea2ba30b3 100644 --- a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb +++ b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb @@ -12,8 +12,8 @@ describe Gitlab::SidekiqDaemon::MemoryKiller do allow(Sidekiq.logger).to receive(:warn) end - describe '#start_working' do - subject { memory_killer.send(:start_working) } + describe '#run_thread' do + subject { memory_killer.send(:run_thread) } before do # let enabled? return 3 times: true, true, false @@ -37,7 +37,7 @@ describe Gitlab::SidekiqDaemon::MemoryKiller do .with( class: described_class.to_s, pid: pid, - message: "Exception from start_working: My Exception") + message: "Exception from run_thread: My Exception") expect(memory_killer).to receive(:rss_within_range?).twice.and_raise(StandardError, 'My Exception') expect(memory_killer).to receive(:sleep).twice.with(Gitlab::SidekiqDaemon::MemoryKiller::CHECK_INTERVAL_SECONDS) @@ -50,7 +50,7 @@ describe Gitlab::SidekiqDaemon::MemoryKiller do .with( class: described_class.to_s, pid: pid, - message: "Exception from start_working: My Exception") + message: "Exception from run_thread: My Exception") expect(memory_killer).to receive(:rss_within_range?).once.and_raise(Exception, 'My Exception') diff --git a/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb b/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb index 397098ed5a4..3f49ef0e9a7 100644 --- a/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb +++ b/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb @@ -37,8 +37,8 @@ describe Gitlab::SidekiqDaemon::Monitor do end end - describe '#start_working when notification channel not enabled' do - subject { monitor.send(:start_working) } + describe '#run_thread when notification channel not enabled' do + subject { monitor.send(:run_thread) } it 'return directly' do allow(monitor).to receive(:notification_channel_enabled?).and_return(nil) @@ -52,8 +52,8 @@ describe Gitlab::SidekiqDaemon::Monitor do end end - describe '#start_working when notification channel enabled' do - subject { monitor.send(:start_working) } + describe '#run_thread when notification channel enabled' do + subject { monitor.send(:run_thread) } before do # we want to run at most once cycle diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index de90e4c2fba..15281c9e826 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -19,17 +19,24 @@ describe Ci::Build do it { is_expected.to belong_to(:runner) } it { is_expected.to belong_to(:trigger_request) } it { is_expected.to belong_to(:erased_by) } + it { is_expected.to have_many(:trace_sections) } it { is_expected.to have_many(:needs) } + it { is_expected.to have_many(:sourced_pipelines) } + it { is_expected.to have_many(:job_variables) } + it { is_expected.to have_one(:deployment) } it { is_expected.to have_one(:runner_session) } - it { is_expected.to have_many(:job_variables) } + it { is_expected.to validate_presence_of(:ref) } + it { is_expected.to respond_to(:has_trace?) } it { is_expected.to respond_to(:trace) } + it { is_expected.to delegate_method(:merge_request_event?).to(:pipeline) } it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) } it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) } + it { is_expected.to include_module(Ci::PipelineDelegator) } describe 'associations' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 0e11c595388..de0ce9932e8 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -28,7 +28,13 @@ describe Ci::Pipeline, :mailer do it { is_expected.to have_many(:builds) } it { is_expected.to have_many(:auto_canceled_pipelines) } it { is_expected.to have_many(:auto_canceled_jobs) } + it { is_expected.to have_many(:sourced_pipelines) } + it { is_expected.to have_many(:triggered_pipelines) } + it { is_expected.to have_one(:chat_data) } + it { is_expected.to have_one(:source_pipeline) } + it { is_expected.to have_one(:triggered_by_pipeline) } + it { is_expected.to have_one(:source_job) } it { is_expected.to validate_presence_of(:sha) } it { is_expected.to validate_presence_of(:status) } diff --git a/spec/models/ci/sources/pipeline_spec.rb b/spec/models/ci/sources/pipeline_spec.rb new file mode 100644 index 00000000000..63bee5bfb55 --- /dev/null +++ b/spec/models/ci/sources/pipeline_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::Sources::Pipeline do + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:pipeline) } + + it { is_expected.to belong_to(:source_project) } + it { is_expected.to belong_to(:source_job) } + it { is_expected.to belong_to(:source_pipeline) } + + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:pipeline) } + + it { is_expected.to validate_presence_of(:source_project) } + it { is_expected.to validate_presence_of(:source_job) } + it { is_expected.to validate_presence_of(:source_pipeline) } +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1490955f4a3..68833fdaf73 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -101,6 +101,8 @@ describe Project do it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) } it { is_expected.to have_many(:cycle_analytics_stages) } it { is_expected.to have_many(:external_pull_requests) } + it { is_expected.to have_many(:sourced_pipelines) } + it { is_expected.to have_many(:source_pipelines) } it 'has an inverse relationship with merge requests' do expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project) diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb index d7c40b8e7b9..b180ede51eb 100644 --- a/spec/serializers/pipeline_details_entity_spec.rb +++ b/spec/serializers/pipeline_details_entity_spec.rb @@ -138,5 +138,40 @@ describe PipelineDetailsEntity do expect(subject[:flags][:yaml_errors]).to be false end end + + context 'when pipeline is triggered by other pipeline' do + let(:pipeline) { create(:ci_empty_pipeline) } + + before do + create(:ci_sources_pipeline, pipeline: pipeline) + end + + it 'contains an information about depedent pipeline' do + expect(subject[:triggered_by]).to be_a(Hash) + expect(subject[:triggered_by][:path]).not_to be_nil + expect(subject[:triggered_by][:details]).not_to be_nil + expect(subject[:triggered_by][:details][:status]).not_to be_nil + expect(subject[:triggered_by][:project]).not_to be_nil + end + end + + context 'when pipeline triggered other pipeline' do + let(:pipeline) { create(:ci_empty_pipeline) } + let(:build) { create(:ci_build, pipeline: pipeline) } + + before do + create(:ci_sources_pipeline, source_job: build) + create(:ci_sources_pipeline, source_job: build) + end + + it 'contains an information about depedent pipeline' do + expect(subject[:triggered]).to be_a(Array) + expect(subject[:triggered].length).to eq(2) + expect(subject[:triggered].first[:path]).not_to be_nil + expect(subject[:triggered].first[:details]).not_to be_nil + expect(subject[:triggered].first[:details][:status]).not_to be_nil + expect(subject[:triggered].first[:project]).not_to be_nil + end + end end end diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index a1d275cfa2a..9762c83ed6a 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -158,7 +158,7 @@ describe PipelineSerializer do it 'verifies number of queries', :request_store do recorded = ActiveRecord::QueryRecorder.new { subject } - expected_queries = Gitlab.ee? ? 38 : 31 + expected_queries = Gitlab.ee? ? 38 : 35 expect(recorded.count).to be_within(2).of(expected_queries) expect(recorded.cached_count).to eq(0) @@ -179,7 +179,8 @@ describe PipelineSerializer do # pipeline. With the same ref this check is cached but if refs are # different then there is an extra query per ref # https://gitlab.com/gitlab-org/gitlab-foss/issues/46368 - expected_queries = Gitlab.ee? ? 44 : 38 + expected_queries = Gitlab.ee? ? 44 : 41 + expect(recorded.count).to be_within(2).of(expected_queries) expect(recorded.cached_count).to eq(0) end diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb index 76251b5b557..24d42f402f4 100644 --- a/spec/services/ci/pipeline_trigger_service_spec.rb +++ b/spec/services/ci/pipeline_trigger_service_spec.rb @@ -11,76 +11,158 @@ describe Ci::PipelineTriggerService do describe '#execute' do let(:user) { create(:user) } - let(:trigger) { create(:ci_trigger, project: project, owner: user) } let(:result) { described_class.new(project, user, params).execute } before do project.add_developer(user) end - context 'when trigger belongs to a different project' do - let(:params) { { token: trigger.token, ref: 'master', variables: nil } } - let(:trigger) { create(:ci_trigger, project: create(:project), owner: user) } + context 'with a trigger token' do + let(:trigger) { create(:ci_trigger, project: project, owner: user) } - it 'does nothing' do - expect { result }.not_to change { Ci::Pipeline.count } - end - end - - context 'when params have an existsed trigger token' do - context 'when params have an existsed ref' do + context 'when trigger belongs to a different project' do let(:params) { { token: trigger.token, ref: 'master', variables: nil } } + let(:trigger) { create(:ci_trigger, project: create(:project), owner: user) } - it 'triggers a pipeline' do - expect { result }.to change { Ci::Pipeline.count }.by(1) - expect(result[:pipeline].ref).to eq('master') - expect(result[:pipeline].project).to eq(project) - expect(result[:pipeline].user).to eq(trigger.owner) - expect(result[:pipeline].trigger_requests.to_a) - .to eq(result[:pipeline].builds.map(&:trigger_request).uniq) - expect(result[:status]).to eq(:success) + it 'does nothing' do + expect { result }.not_to change { Ci::Pipeline.count } end + end - context 'when commit message has [ci skip]' do - before do - allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { '[ci skip]' } - end + context 'when params have an existsed trigger token' do + context 'when params have an existsed ref' do + let(:params) { { token: trigger.token, ref: 'master', variables: nil } } - it 'ignores [ci skip] and create as general' do + it 'triggers a pipeline' do expect { result }.to change { Ci::Pipeline.count }.by(1) + expect(result[:pipeline].ref).to eq('master') + expect(result[:pipeline].project).to eq(project) + expect(result[:pipeline].user).to eq(trigger.owner) + expect(result[:pipeline].trigger_requests.to_a) + .to eq(result[:pipeline].builds.map(&:trigger_request).uniq) expect(result[:status]).to eq(:success) end + + context 'when commit message has [ci skip]' do + before do + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { '[ci skip]' } + end + + it 'ignores [ci skip] and create as general' do + expect { result }.to change { Ci::Pipeline.count }.by(1) + expect(result[:status]).to eq(:success) + end + end + + context 'when params have a variable' do + let(:params) { { token: trigger.token, ref: 'master', variables: variables } } + let(:variables) { { 'AAA' => 'AAA123' } } + + it 'has a variable' do + expect { result }.to change { Ci::PipelineVariable.count }.by(1) + .and change { Ci::TriggerRequest.count }.by(1) + expect(result[:pipeline].variables.map { |v| { v.key => v.value } }.first).to eq(variables) + expect(result[:pipeline].trigger_requests.last.variables).to be_nil + end + end end - context 'when params have a variable' do - let(:params) { { token: trigger.token, ref: 'master', variables: variables } } - let(:variables) { { 'AAA' => 'AAA123' } } + context 'when params have a non-existsed ref' do + let(:params) { { token: trigger.token, ref: 'invalid-ref', variables: nil } } - it 'has a variable' do - expect { result }.to change { Ci::PipelineVariable.count }.by(1) - .and change { Ci::TriggerRequest.count }.by(1) - expect(result[:pipeline].variables.map { |v| { v.key => v.value } }.first).to eq(variables) - expect(result[:pipeline].trigger_requests.last.variables).to be_nil + it 'does not trigger a pipeline' do + expect { result }.not_to change { Ci::Pipeline.count } + expect(result[:http_status]).to eq(400) end end end - context 'when params have a non-existsed ref' do - let(:params) { { token: trigger.token, ref: 'invalid-ref', variables: nil } } + context 'when params have a non-existsed trigger token' do + let(:params) { { token: 'invalid-token', ref: nil, variables: nil } } it 'does not trigger a pipeline' do expect { result }.not_to change { Ci::Pipeline.count } - expect(result[:http_status]).to eq(400) + expect(result).to be_nil end end end - context 'when params have a non-existsed trigger token' do - let(:params) { { token: 'invalid-token', ref: nil, variables: nil } } + context 'with a pipeline job token' do + let!(:pipeline) { create(:ci_empty_pipeline, project: project) } + let(:job) { create(:ci_build, :running, pipeline: pipeline, user: user) } + + context 'when job user does not have a permission to read a project' do + let(:params) { { token: job.token, ref: 'master', variables: nil } } + let(:job) { create(:ci_build, pipeline: pipeline, user: create(:user)) } + + it 'does nothing' do + expect { result }.not_to change { Ci::Pipeline.count } + end + end + + context 'when job is not running' do + let(:params) { { token: job.token, ref: 'master', variables: nil } } + let(:job) { create(:ci_build, :success, pipeline: pipeline, user: user) } + + it 'does nothing' do + expect { result }.not_to change { Ci::Pipeline.count } + expect(result[:message]).to eq('400 Job has to be running') + end + end - it 'does not trigger a pipeline' do - expect { result }.not_to change { Ci::Pipeline.count } - expect(result).to be_nil + context 'when params have an existsed job token' do + context 'when params have an existsed ref' do + let(:params) { { token: job.token, ref: 'master', variables: nil } } + + it 'triggers a pipeline' do + expect { result }.to change { Ci::Pipeline.count }.by(1) + expect(result[:pipeline].ref).to eq('master') + expect(result[:pipeline].project).to eq(project) + expect(result[:pipeline].user).to eq(job.user) + expect(result[:status]).to eq(:success) + end + + context 'when commit message has [ci skip]' do + before do + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { '[ci skip]' } + end + + it 'ignores [ci skip] and create as general' do + expect { result }.to change { Ci::Pipeline.count }.by(1) + expect(result[:status]).to eq(:success) + end + end + + context 'when params have a variable' do + let(:params) { { token: job.token, ref: 'master', variables: variables } } + let(:variables) { { 'AAA' => 'AAA123' } } + + it 'has a variable' do + expect { result }.to change { Ci::PipelineVariable.count }.by(1) + .and change { Ci::Sources::Pipeline.count }.by(1) + expect(result[:pipeline].variables.map { |v| { v.key => v.value } }.first).to eq(variables) + expect(job.sourced_pipelines.last.pipeline_id).to eq(result[:pipeline].id) + end + end + end + + context 'when params have a non-existsed ref' do + let(:params) { { token: job.token, ref: 'invalid-ref', variables: nil } } + + it 'does not job a pipeline' do + expect { result }.not_to change { Ci::Pipeline.count } + expect(result[:http_status]).to eq(400) + end + end + end + + context 'when params have a non-existsed trigger token' do + let(:params) { { token: 'invalid-token', ref: nil, variables: nil } } + + it 'does not trigger a pipeline' do + expect { result }.not_to change { Ci::Pipeline.count } + expect(result).to be_nil + end end end end diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index 0cbb3122bb0..5ef1fb1932f 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -426,5 +426,22 @@ describe Groups::TransferService do end end end + + context 'when a project in group has container images' do + let(:group) { create(:group, :public, :nested) } + let!(:project) { create(:project, :repository, :public, namespace: group) } + + before do + stub_container_registry_tags(repository: /image/, tags: %w[rc1]) + create(:container_repository, project: project, name: :image) + create(:group_member, :owner, group: new_parent_group, user: user) + end + + it 'does not allow group to be transferred' do + transfer_service.execute(new_parent_group) + + expect(transfer_service.error).to match(/Docker images in their Container Registry/) + end + end end end diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 12e9c2b2f3a..ca8eaf4c970 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -148,6 +148,30 @@ describe Groups::UpdateService do end end + context 'projects in group have container images' do + let(:service) { described_class.new(public_group, user, path: SecureRandom.hex) } + let(:project) { create(:project, :internal, group: public_group) } + + before do + stub_container_registry_tags(repository: /image/, tags: %w[rc1]) + create(:container_repository, project: project, name: :image) + end + + it 'does not allow path to be changed' do + result = described_class.new(public_group, user, path: 'new-path').execute + + expect(result).to eq false + expect(public_group.errors[:base].first).to match(/Docker images in their Container Registry/) + end + + it 'allows other settings to be changed' do + result = described_class.new(public_group, user, name: 'new-name').execute + + expect(result).to eq true + expect(public_group.reload.name).to eq('new-name') + end + end + context 'for a subgroup' do let(:subgroup) { create(:group, :private, parent: private_group) } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 948e5e6250b..61e50fe5723 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -154,6 +154,17 @@ RSpec.configure do |config| .with(:force_autodevops_on_by_default, anything) .and_return(false) + # The following can be removed once Vue Issuable Sidebar + # is feature-complete and can be made default in place + # of older sidebar. + # See https://gitlab.com/groups/gitlab-org/-/epics/1863 + allow(Feature).to receive(:enabled?) + .with(:vue_issuable_sidebar, anything) + .and_return(false) + allow(Feature).to receive(:enabled?) + .with(:vue_issuable_epic_sidebar, anything) + .and_return(false) + # Stub these calls due to being expensive operations # It can be reenabled for specific tests via: # |