diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-18 12:09:13 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-18 12:09:13 +0000 |
commit | 1363ca12f1f07c634647cf55c4c16b7401098673 (patch) | |
tree | d932caf09c8148322edb51ae954ed159ff7d00f8 | |
parent | 6763d2787670bc03a36a8eb601703e88fc70dece (diff) | |
download | gitlab-ce-1363ca12f1f07c634647cf55c4c16b7401098673.tar.gz |
Add latest changes from gitlab-org/gitlab@master
61 files changed, 1069 insertions, 157 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index 8006e45259a..3bbe3020b70 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -750,7 +750,7 @@ GEM orm_adapter (0.5.0) os (1.0.0) parallel (1.19.1) - parser (2.6.5.0) + parser (2.7.0.4) ast (~> 2.4.0) parslet (1.8.2) peek (1.1.0) @@ -1094,13 +1094,13 @@ GEM uniform_notifier (1.13.0) unleash (0.1.5) murmurhash3 (~> 0.1.6) - unparser (0.4.5) + unparser (0.4.7) abstract_type (~> 0.0.7) adamantium (~> 0.2.0) concord (~> 0.1.5) diff-lcs (~> 1.3) equalizer (~> 0.0.9) - parser (~> 2.6.3) + parser (>= 2.6.5) procto (~> 0.0.2) validate_email (0.1.6) activemodel (>= 3.0) diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue index be70bfc7399..03766c4877e 100644 --- a/app/assets/javascripts/repository/components/breadcrumbs.vue +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -1,6 +1,6 @@ <script> import { GlDropdown, GlDropdownDivider, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui'; -import { joinPaths } from '~/lib/utils/url_utility'; +import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import { __ } from '../../locale'; import Icon from '../../vue_shared/components/icon.vue'; import getRefMixin from '../mixins/get_ref'; @@ -103,7 +103,7 @@ export default { .filter(p => p !== '') .reduce( (acc, name, i) => { - const path = joinPaths(i > 0 ? acc[i].path : '', encodeURIComponent(name)); + const path = joinPaths(i > 0 ? acc[i].path : '', escapeFileUrl(name)); return acc.concat({ name, diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index b81e6a38b4c..f3e6e3686a3 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -1,7 +1,7 @@ <script> import { escapeRegExp } from 'lodash'; import { GlBadge, GlLink, GlSkeletonLoading, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui'; -import { visitUrl } from '~/lib/utils/url_utility'; +import { visitUrl, escapeFileUrl } from '~/lib/utils/url_utility'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import Icon from '~/vue_shared/components/icon.vue'; import { getIconName } from '../../utils/icon'; @@ -92,7 +92,7 @@ export default { computed: { routerLinkTo() { return this.isFolder - ? { path: `/-/tree/${escape(this.ref)}/${encodeURIComponent(this.path)}` } + ? { path: `/-/tree/${escape(this.ref)}/${escapeFileUrl(this.path)}` } : null; }, iconName() { diff --git a/app/controllers/projects/import/jira_controller.rb b/app/controllers/projects/import/jira_controller.rb index c74c180fa20..d38d9e27347 100644 --- a/app/controllers/projects/import/jira_controller.rb +++ b/app/controllers/projects/import/jira_controller.rb @@ -42,11 +42,13 @@ module Projects def schedule_import(params) import_data = @project.create_or_update_import_data(data: {}).becomes(JiraImportData) - import_data << JiraImportData::JiraProjectDetails.new( + jira_project_details = JiraImportData::JiraProjectDetails.new( params[:jira_project_key], Time.now.strftime('%Y-%m-%d %H:%M:%S'), { user_id: current_user.id, name: current_user.name } ) + import_data << jira_project_details + import_data.force_import! @project.import_type = 'jira' @project.import_state.schedule if @project.save diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index cc5ae32856a..4debf66db64 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -344,8 +344,8 @@ module BlobHelper def show_suggest_pipeline_creation_celebration? experiment_enabled?(:suggest_pipeline) && - @blob.auxiliary_viewer.valid?(project: @project, sha: @commit.sha, user: current_user) && @blob.path == Gitlab::FileDetector::PATTERNS[:gitlab_ci] && + @blob.auxiliary_viewer.valid?(project: @project, sha: @commit.sha, user: current_user) && @project.uses_default_ci_config? && cookies[suggest_pipeline_commit_cookie_name].present? end diff --git a/app/helpers/ci_variables_helper.rb b/app/helpers/ci_variables_helper.rb index 3f4c04070b5..b271f069778 100644 --- a/app/helpers/ci_variables_helper.rb +++ b/app/helpers/ci_variables_helper.rb @@ -45,6 +45,6 @@ module CiVariablesHelper end def ci_variable_maskable_regex - Maskable::REGEX.inspect.sub('\\A', '^').sub('\\z', '$').sub(/^\//, '').sub(/\/[a-z]*$/, '').gsub('\/', '/') + Ci::Maskable::REGEX.inspect.sub('\\A', '^').sub('\\z', '$').sub(/^\//, '').sub(/\/[a-z]*$/, '').gsub('\/', '/') end end diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb index 0e50265c7ba..1e1dd68ee6c 100644 --- a/app/models/ci/group_variable.rb +++ b/app/models/ci/group_variable.rb @@ -3,9 +3,9 @@ module Ci class GroupVariable < ApplicationRecord extend Gitlab::Ci::Model - include HasVariable + include Ci::HasVariable include Presentable - include Maskable + include Ci::Maskable belongs_to :group, class_name: "::Group" diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb index f2968c037c7..7eea8a37150 100644 --- a/app/models/ci/job_variable.rb +++ b/app/models/ci/job_variable.rb @@ -3,7 +3,7 @@ module Ci class JobVariable < ApplicationRecord extend Gitlab::Ci::Model - include NewHasVariable + include Ci::NewHasVariable include BulkInsertSafe belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb index be6e5e76c31..adef9911ae1 100644 --- a/app/models/ci/pipeline_schedule_variable.rb +++ b/app/models/ci/pipeline_schedule_variable.rb @@ -3,7 +3,7 @@ module Ci class PipelineScheduleVariable < ApplicationRecord extend Gitlab::Ci::Model - include HasVariable + include Ci::HasVariable belongs_to :pipeline_schedule diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb index 51a6272e1ff..84ca4833cd7 100644 --- a/app/models/ci/pipeline_variable.rb +++ b/app/models/ci/pipeline_variable.rb @@ -3,7 +3,7 @@ module Ci class PipelineVariable < ApplicationRecord extend Gitlab::Ci::Model - include HasVariable + include Ci::HasVariable belongs_to :pipeline diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 760872d3e6b..08d39595c61 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -3,9 +3,9 @@ module Ci class Variable < ApplicationRecord extend Gitlab::Ci::Model - include HasVariable + include Ci::HasVariable include Presentable - include Maskable + include Ci::Maskable prepend HasEnvironmentScope belongs_to :project diff --git a/app/models/concerns/ci/has_variable.rb b/app/models/concerns/ci/has_variable.rb new file mode 100644 index 00000000000..9bf2b409080 --- /dev/null +++ b/app/models/concerns/ci/has_variable.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Ci + module HasVariable + extend ActiveSupport::Concern + + included do + enum variable_type: { + env_var: 1, + file: 2 + } + + validates :key, + presence: true, + length: { maximum: 255 }, + format: { with: /\A[a-zA-Z0-9_]+\z/, + message: "can contain only letters, digits and '_'." } + + scope :order_key_asc, -> { reorder(key: :asc) } + + attr_encrypted :value, + mode: :per_attribute_iv_and_salt, + insecure_mode: true, + key: Settings.attr_encrypted_db_key_base, + algorithm: 'aes-256-cbc' + + def key=(new_key) + super(new_key.to_s.strip) + end + end + + def to_runner_variable + { key: key, value: value, public: false, file: file? } + end + end +end diff --git a/app/models/concerns/ci/maskable.rb b/app/models/concerns/ci/maskable.rb new file mode 100644 index 00000000000..15bc48bf964 --- /dev/null +++ b/app/models/concerns/ci/maskable.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Ci + module Maskable + extend ActiveSupport::Concern + + # * Single line + # * No escape characters + # * No variables + # * No spaces + # * Minimal length of 8 characters + # * Characters must be from the Base64 alphabet (RFC4648) with the addition of @ and : + # * Absolutely no fun is allowed + REGEX = /\A[a-zA-Z0-9_+=\/@:-]{8,}\z/.freeze + + included do + validates :masked, inclusion: { in: [true, false] } + validates :value, format: { with: REGEX }, if: :masked? + end + + def to_runner_variable + super.merge(masked: masked?) + end + end +end diff --git a/app/models/concerns/ci/new_has_variable.rb b/app/models/concerns/ci/new_has_variable.rb new file mode 100644 index 00000000000..546d243e5de --- /dev/null +++ b/app/models/concerns/ci/new_has_variable.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Ci + module NewHasVariable + extend ActiveSupport::Concern + include Ci::HasVariable + + included do + attr_encrypted :value, + mode: :per_attribute_iv, + algorithm: 'aes-256-gcm', + key: Settings.attr_encrypted_db_key_base_32, + insecure_mode: false + end + end +end diff --git a/app/models/concerns/has_variable.rb b/app/models/concerns/has_variable.rb deleted file mode 100644 index b4e99569071..00000000000 --- a/app/models/concerns/has_variable.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module HasVariable - extend ActiveSupport::Concern - - included do - enum variable_type: { - env_var: 1, - file: 2 - } - - validates :key, - presence: true, - length: { maximum: 255 }, - format: { with: /\A[a-zA-Z0-9_]+\z/, - message: "can contain only letters, digits and '_'." } - - scope :order_key_asc, -> { reorder(key: :asc) } - - attr_encrypted :value, - mode: :per_attribute_iv_and_salt, - insecure_mode: true, - key: Settings.attr_encrypted_db_key_base, - algorithm: 'aes-256-cbc' - - def key=(new_key) - super(new_key.to_s.strip) - end - end - - def to_runner_variable - { key: key, value: value, public: false, file: file? } - end -end diff --git a/app/models/concerns/maskable.rb b/app/models/concerns/maskable.rb deleted file mode 100644 index d70e47bc4ff..00000000000 --- a/app/models/concerns/maskable.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module Maskable - extend ActiveSupport::Concern - - # * Single line - # * No escape characters - # * No variables - # * No spaces - # * Minimal length of 8 characters - # * Characters must be from the Base64 alphabet (RFC4648) with the addition of @ and : - # * Absolutely no fun is allowed - REGEX = /\A[a-zA-Z0-9_+=\/@:-]{8,}\z/.freeze - - included do - validates :masked, inclusion: { in: [true, false] } - validates :value, format: { with: REGEX }, if: :masked? - end - - def to_runner_variable - super.merge(masked: masked?) - end -end diff --git a/app/models/concerns/new_has_variable.rb b/app/models/concerns/new_has_variable.rb deleted file mode 100644 index 429bf496872..00000000000 --- a/app/models/concerns/new_has_variable.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -module NewHasVariable - extend ActiveSupport::Concern - include HasVariable - - included do - attr_encrypted :value, - mode: :per_attribute_iv, - algorithm: 'aes-256-gcm', - key: Settings.attr_encrypted_db_key_base_32, - insecure_mode: false - end -end diff --git a/app/models/jira_import_data.rb b/app/models/jira_import_data.rb index 3f882deb24d..63be190aa0d 100644 --- a/app/models/jira_import_data.rb +++ b/app/models/jira_import_data.rb @@ -3,17 +3,40 @@ class JiraImportData < ProjectImportData JiraProjectDetails = Struct.new(:key, :scheduled_at, :scheduled_by) + FORCE_IMPORT_KEY = 'force-import' + def projects return [] unless data - projects = data.dig('jira', 'projects').map do |p| + projects = data.dig('jira', 'projects')&.map do |p| JiraProjectDetails.new(p['key'], p['scheduled_at'], p['scheduled_by']) end - projects.sort_by { |jp| jp.scheduled_at } + + projects&.sort_by { |jp| jp.scheduled_at } || [] end def <<(project) - self.data ||= { jira: { projects: [] } } - self.data['jira']['projects'] << project.to_h.deep_stringify_keys! + self.data ||= { 'jira' => { 'projects' => [] } } + self.data['jira'] ||= { 'projects' => [] } + self.data['jira']['projects'] = [] if data['jira']['projects'].blank? || !data['jira']['projects'].is_a?(Array) + + self.data['jira']['projects'] << project.to_h + self.data.deep_stringify_keys! + end + + def force_import! + self.data ||= {} + self.data.deep_merge!({ 'jira' => { FORCE_IMPORT_KEY => true } }) + self.data.deep_stringify_keys! + end + + def force_import? + !!data&.dig('jira', FORCE_IMPORT_KEY) && !projects.blank? + end + + def finish_import! + return if data&.dig('jira', FORCE_IMPORT_KEY).nil? + + data['jira'].delete(FORCE_IMPORT_KEY) end end diff --git a/app/models/project.rb b/app/models/project.rb index 4892c5310ec..7e006e734c5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -868,6 +868,8 @@ class Project < ApplicationRecord elsif gitlab_project_import? # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-foss/issues/26189 is solved. RepositoryImportWorker.set(retry: false).perform_async(self.id) + elsif jira_import? + Gitlab::JiraImport::Stage::StartImportWorker.perform_async(self.id) else RepositoryImportWorker.perform_async(self.id) end @@ -900,7 +902,7 @@ class Project < ApplicationRecord # This method is overridden in EE::Project model def remove_import_data - import_data&.destroy + import_data&.destroy unless jira_import? end def ci_config_path=(value) @@ -947,7 +949,7 @@ class Project < ApplicationRecord end def import? - external_import? || forked? || gitlab_project_import? || bare_repository_import? + external_import? || forked? || gitlab_project_import? || jira_import? || bare_repository_import? end def external_import? @@ -962,6 +964,14 @@ class Project < ApplicationRecord import_type == 'bare_repository' end + def jira_import? + import_type == 'jira' && Feature.enabled?(:jira_issue_import, self) + end + + def jira_force_import? + jira_import? && import_data&.becomes(JiraImportData)&.force_import? + end + def gitlab_project_import? import_type == 'gitlab_project' end diff --git a/app/services/milestones/transfer_service.rb b/app/services/milestones/transfer_service.rb index 213c6f8f1dd..18d7e41adc7 100644 --- a/app/services/milestones/transfer_service.rb +++ b/app/services/milestones/transfer_service.rb @@ -46,7 +46,7 @@ module Milestones Milestone.joins(:issues) .where( issues: { project_id: project.id }, - group_id: old_group.id + group_id: old_group.self_and_ancestors ) end # rubocop: enable CodeReuse/ActiveRecord @@ -56,7 +56,7 @@ module Milestones Milestone.joins(:merge_requests) .where( merge_requests: { target_project_id: project.id }, - group_id: old_group.id + group_id: old_group.self_and_ancestors ) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 3df86e3314d..dd0eeaa9359 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -528,6 +528,55 @@ :resource_boundary: :unknown :weight: 2 :idempotent: +- :name: jira_importer:jira_import_advance_stage + :feature_category: :importers + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: +- :name: jira_importer:jira_import_stage_finish_import + :feature_category: :importers + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: +- :name: jira_importer:jira_import_stage_import_attachments + :feature_category: :importers + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: +- :name: jira_importer:jira_import_stage_import_issues + :feature_category: :importers + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: +- :name: jira_importer:jira_import_stage_import_labels + :feature_category: :importers + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: +- :name: jira_importer:jira_import_stage_import_notes + :feature_category: :importers + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: +- :name: jira_importer:jira_import_stage_start_import + :feature_category: :importers + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: - :name: mail_scheduler:mail_scheduler_issue_due :feature_category: :issue_tracking :has_external_dependencies: diff --git a/app/workers/concerns/gitlab/jira_import/import_worker.rb b/app/workers/concerns/gitlab/jira_import/import_worker.rb new file mode 100644 index 00000000000..7cc650bfc29 --- /dev/null +++ b/app/workers/concerns/gitlab/jira_import/import_worker.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Gitlab + module JiraImport + module ImportWorker + extend ActiveSupport::Concern + + included do + include ApplicationWorker + include Gitlab::JiraImport::QueueOptions + end + + def perform(project_id) + project = Project.find_by(id: project_id) # rubocop: disable CodeReuse/ActiveRecord + + return unless can_import?(project) + + import(project) + end + + private + + def import(project) + raise NotImplementedError + end + + def can_import?(project) + return false unless project + return false if Feature.disabled?(:jira_issue_import, project) + + project.import_state.started? + end + end + end +end diff --git a/app/workers/concerns/gitlab/jira_import/queue_options.rb b/app/workers/concerns/gitlab/jira_import/queue_options.rb new file mode 100644 index 00000000000..bc1148f7d3b --- /dev/null +++ b/app/workers/concerns/gitlab/jira_import/queue_options.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Gitlab + module JiraImport + module QueueOptions + extend ActiveSupport::Concern + + included do + queue_namespace :jira_importer + feature_category :importers + + sidekiq_options retry: 5 + end + end + end +end diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb index 8fbf88a1762..8bbfb10ed6e 100644 --- a/app/workers/gitlab/github_import/advance_stage_worker.rb +++ b/app/workers/gitlab/github_import/advance_stage_worker.rb @@ -13,8 +13,6 @@ module Gitlab sidekiq_options dead: false feature_category :importers - private - # The known importer stages and their corresponding Sidekiq workers. STAGES = { issues_and_diff_notes: Stage::ImportIssuesAndDiffNotesWorker, @@ -23,6 +21,8 @@ module Gitlab finish: Stage::FinishImportWorker }.freeze + private + def next_stage_worker(next_stage) STAGES.fetch(next_stage.to_sym) end diff --git a/app/workers/gitlab/jira_import/advance_stage_worker.rb b/app/workers/gitlab/jira_import/advance_stage_worker.rb new file mode 100644 index 00000000000..1b6fc54151e --- /dev/null +++ b/app/workers/gitlab/jira_import/advance_stage_worker.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Gitlab + module JiraImport + class AdvanceStageWorker # rubocop:disable Scalability/IdempotentWorker + include ApplicationWorker + include QueueOptions + include ::Gitlab::Import::AdvanceStage + + # The known importer stages and their corresponding Sidekiq workers. + STAGES = { + labels: Gitlab::JiraImport::Stage::ImportLabelsWorker, + issues: Gitlab::JiraImport::Stage::ImportIssuesWorker, + attachments: Gitlab::JiraImport::Stage::ImportAttachmentsWorker, + notes: Gitlab::JiraImport::Stage::ImportNotesWorker, + finish: Gitlab::JiraImport::Stage::FinishImportWorker + }.freeze + + private + + def next_stage_worker(next_stage) + STAGES.fetch(next_stage.to_sym) + end + end + end +end diff --git a/app/workers/gitlab/jira_import/stage/finish_import_worker.rb b/app/workers/gitlab/jira_import/stage/finish_import_worker.rb new file mode 100644 index 00000000000..5b1661d68c6 --- /dev/null +++ b/app/workers/gitlab/jira_import/stage/finish_import_worker.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Gitlab + module JiraImport + module Stage + class FinishImportWorker # rubocop:disable Scalability/IdempotentWorker + include Gitlab::JiraImport::ImportWorker + + private + + def import(project) + project.after_import + ensure + project.import_data.becomes(JiraImportData).finish_import! + project.import_data.save! + end + end + end + end +end diff --git a/app/workers/gitlab/jira_import/stage/import_attachments_worker.rb b/app/workers/gitlab/jira_import/stage/import_attachments_worker.rb new file mode 100644 index 00000000000..3b209a279b5 --- /dev/null +++ b/app/workers/gitlab/jira_import/stage/import_attachments_worker.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module JiraImport + module Stage + class ImportAttachmentsWorker # rubocop:disable Scalability/IdempotentWorker + include Gitlab::JiraImport::ImportWorker + + private + + def import(project) + # fake a attahcments import workers for now. + # new job waiter will have zero jobs_remaining by default, so it will just pass on to next stage + fake_waiter = JobWaiter.new + + project.import_state.refresh_jid_expiration + Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { fake_waiter.key => fake_waiter.jobs_remaining }, :notes) + end + end + end + end +end diff --git a/app/workers/gitlab/jira_import/stage/import_issues_worker.rb b/app/workers/gitlab/jira_import/stage/import_issues_worker.rb new file mode 100644 index 00000000000..79ed8e1f2da --- /dev/null +++ b/app/workers/gitlab/jira_import/stage/import_issues_worker.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module JiraImport + module Stage + class ImportIssuesWorker # rubocop:disable Scalability/IdempotentWorker + include Gitlab::JiraImport::ImportWorker + + private + + def import(project) + # fake issues import workers for now + # new job waiter will have zero jobs_remaining by default, so it will just pass on to next stage + jobs_waiter = JobWaiter.new + project.import_state.refresh_jid_expiration + + Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { jobs_waiter.key => jobs_waiter.jobs_remaining }, :attachments) + end + end + end + end +end diff --git a/app/workers/gitlab/jira_import/stage/import_labels_worker.rb b/app/workers/gitlab/jira_import/stage/import_labels_worker.rb new file mode 100644 index 00000000000..b96bb1bbdda --- /dev/null +++ b/app/workers/gitlab/jira_import/stage/import_labels_worker.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Gitlab + module JiraImport + module Stage + class ImportLabelsWorker # rubocop:disable Scalability/IdempotentWorker + include Gitlab::JiraImport::ImportWorker + + private + + def import(project) + # fake labels import workers for now + # new job waiter will have zero jobs_remaining by default, so it will just pass on to next stage + fake_waiter = JobWaiter.new + Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { fake_waiter.key => fake_waiter.jobs_remaining }, :issues) + end + end + end + end +end diff --git a/app/workers/gitlab/jira_import/stage/import_notes_worker.rb b/app/workers/gitlab/jira_import/stage/import_notes_worker.rb new file mode 100644 index 00000000000..9eef0d31a8c --- /dev/null +++ b/app/workers/gitlab/jira_import/stage/import_notes_worker.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module JiraImport + module Stage + class ImportNotesWorker # rubocop:disable Scalability/IdempotentWorker + include Gitlab::JiraImport::ImportWorker + + private + + def import(project) + # fake notes import workers for now + # new job waiter will have zero jobs_remaining by default, so it will just pass on to next stage + jobs_waiter = JobWaiter.new + project.import_state.refresh_jid_expiration + + Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { jobs_waiter.key => jobs_waiter.jobs_remaining }, :finish) + end + end + end + end +end diff --git a/app/workers/gitlab/jira_import/stage/start_import_worker.rb b/app/workers/gitlab/jira_import/stage/start_import_worker.rb new file mode 100644 index 00000000000..8abbfab647b --- /dev/null +++ b/app/workers/gitlab/jira_import/stage/start_import_worker.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Gitlab + module JiraImport + module Stage + class StartImportWorker # rubocop:disable Scalability/IdempotentWorker + include ApplicationWorker + include ProjectStartImport + include ProjectImportOptions + include Gitlab::JiraImport::QueueOptions + + attr_reader :project + + def perform(project_id) + @project = Project.find_by(id: project_id) # rubocop: disable CodeReuse/ActiveRecord + + return unless start_import + + Gitlab::Import::SetAsyncJid.set_jid(project) + + Gitlab::JiraImport::Stage::ImportLabelsWorker.perform_async(project.id) + end + + private + + def start_import + return false unless project + return false if Feature.disabled?(:jira_issue_import, project) + return true if start(project.import_state) + + Gitlab::Import::Logger.info( + { + project_id: project.id, + project_path: project.full_path, + state: project&.import_status, + message: 'inconsistent state while importing' + } + ) + false + end + end + end + end +end diff --git a/changelogs/unreleased/208894-fix-showing-only-free-namespaces-when-applying-licenses.yml b/changelogs/unreleased/208894-fix-showing-only-free-namespaces-when-applying-licenses.yml new file mode 100644 index 00000000000..40495fc2d06 --- /dev/null +++ b/changelogs/unreleased/208894-fix-showing-only-free-namespaces-when-applying-licenses.yml @@ -0,0 +1,5 @@ +--- +title: Fix managed_free_namespaces scope to only groups without a license or a free license +merge_request: 27356 +author: +type: fixed diff --git a/changelogs/unreleased/208923-enable-batch-counting-for-some-individual-queries-5.yml b/changelogs/unreleased/208923-enable-batch-counting-for-some-individual-queries-5.yml new file mode 100644 index 00000000000..93f07635a31 --- /dev/null +++ b/changelogs/unreleased/208923-enable-batch-counting-for-some-individual-queries-5.yml @@ -0,0 +1,5 @@ +--- +title: Optimize projects_mirrored_with_pipelines_enabled query performance in usage data +merge_request: 27110 +author: +type: performance diff --git a/changelogs/unreleased/fix-invalid-milestones-when-moving-projects.yml b/changelogs/unreleased/fix-invalid-milestones-when-moving-projects.yml new file mode 100644 index 00000000000..24a0e303176 --- /dev/null +++ b/changelogs/unreleased/fix-invalid-milestones-when-moving-projects.yml @@ -0,0 +1,5 @@ +--- +title: Fix invalid ancestor group milestones when moving projects +merge_request: 27262 +author: +type: fixed diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 81085d4641e..eb41e4ac423 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -459,6 +459,11 @@ production: &base elastic_index_bulk_cron_worker: cron: "*/1 * * * *" + # Elasticsearch metrics + # NOTE: This will only take effect if Elasticsearch is enabled. + elastic_metrics_update_worker: + cron: "*/1 * * * *" + registry: # enabled: true # host: registry.example.com diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 8d88d1bcf7c..81de0ac6818 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -546,6 +546,9 @@ Gitlab.ee do Settings.cron_jobs['elastic_index_bulk_cron_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['elastic_index_bulk_cron_worker']['cron'] ||= '*/1 * * * *' Settings.cron_jobs['elastic_index_bulk_cron_worker']['job_class'] ||= 'ElasticIndexBulkCronWorker' + Settings.cron_jobs['elastic_metrics_update_worker'] ||= Settingslogic.new({}) + Settings.cron_jobs['elastic_metrics_update_worker']['cron'] ||= '*/1 * * * *' + Settings.cron_jobs['elastic_metrics_update_worker']['job_class'] ||= 'ElasticMetricsUpdateWorker' Settings.cron_jobs['sync_seat_link_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['sync_seat_link_worker']['cron'] ||= "#{rand(60)} 0 * * *" Settings.cron_jobs['sync_seat_link_worker']['job_class'] = 'SyncSeatLinkWorker' diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 2dc2f33e71e..858766f19ca 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -128,6 +128,8 @@ - 1 - - jira_connect - 1 +- - jira_importer + - 1 - - ldap_group_sync - 2 - - mail_scheduler diff --git a/db/migrate/20200311141943_insert_ci_pipeline_schedules_plan_limits.rb b/db/migrate/20200311141943_insert_ci_pipeline_schedules_plan_limits.rb index d1ad5be5f85..849d95667a7 100644 --- a/db/migrate/20200311141943_insert_ci_pipeline_schedules_plan_limits.rb +++ b/db/migrate/20200311141943_insert_ci_pipeline_schedules_plan_limits.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class InsertCiPipelineSchedulesPlanLimits < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + DOWNTIME = false def change diff --git a/db/migrate/20200312160532_add_index_on_mirror_and_id_to_projects.rb b/db/migrate/20200312160532_add_index_on_mirror_and_id_to_projects.rb new file mode 100644 index 00000000000..3a64b915931 --- /dev/null +++ b/db/migrate/20200312160532_add_index_on_mirror_and_id_to_projects.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class AddIndexOnMirrorAndIdToProjects < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + OLD_INDEX_NAME = 'index_projects_on_mirror_and_mirror_trigger_builds_both_true' + NEW_INDEX_NAME = 'index_projects_on_mirror_id_where_mirror_and_trigger_builds' + + disable_ddl_transaction! + + def up + add_concurrent_index :projects, :id, where: 'mirror = TRUE AND mirror_trigger_builds = TRUE', name: NEW_INDEX_NAME + remove_concurrent_index_by_name :projects, OLD_INDEX_NAME + end + + def down + add_concurrent_index :projects, :id, where: 'mirror IS TRUE AND mirror_trigger_builds IS TRUE', name: OLD_INDEX_NAME + remove_concurrent_index_by_name :projects, NEW_INDEX_NAME + end +end diff --git a/db/schema.rb b/db/schema.rb index 1e32f4e6bd4..9edc1d9853c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -3507,7 +3507,7 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do t.index ["id"], name: "index_on_id_partial_with_legacy_storage", where: "((storage_version < 2) OR (storage_version IS NULL))" t.index ["id"], name: "index_projects_on_id_partial_for_visibility", unique: true, where: "(visibility_level = ANY (ARRAY[10, 20]))" t.index ["id"], name: "index_projects_on_id_service_desk_enabled", where: "(service_desk_enabled = true)" - t.index ["id"], name: "index_projects_on_mirror_and_mirror_trigger_builds_both_true", where: "((mirror IS TRUE) AND (mirror_trigger_builds IS TRUE))" + t.index ["id"], name: "index_projects_on_mirror_id_where_mirror_and_trigger_builds", where: "((mirror = true) AND (mirror_trigger_builds = true))" t.index ["last_activity_at", "id"], name: "index_projects_api_last_activity_at_id_desc", order: { id: :desc } t.index ["last_activity_at", "id"], name: "index_projects_api_vis20_last_activity_at", where: "(visibility_level = 20)" t.index ["last_activity_at", "id"], name: "index_projects_on_last_activity_at_and_id" diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index b789f2ddd02..186b848f8bd 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -17,7 +17,13 @@ GitLab monitors its own internal service metrics, and makes them available at th `/-/metrics` endpoint. Unlike other [Prometheus](https://prometheus.io) exporters, in order to access it, the client IP needs to be [included in a whitelist](../ip_whitelist.md). -For Omnibus and Chart installations, these metrics are automatically enabled and collected as of [GitLab 9.4](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/1702). For source installations or earlier versions, these metrics will need to be enabled manually and collected by a Prometheus server. +For Omnibus and Chart installations, these metrics are automatically enabled +and collected as of [GitLab +9.4](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/1702). For +source installations or earlier versions, these metrics will need to be enabled +manually and collected by a Prometheus server. + +See also [Sidekiq metrics](#sidekiq-metrics) for how to enable and view metrics from Sidekiq nodes. ## Metrics available @@ -105,10 +111,12 @@ The following metrics can be controlled by feature flags: | `gitlab_method_call_duration_seconds` | `prometheus_metrics_method_instrumentation` | | `gitlab_view_rendering_duration_seconds` | `prometheus_metrics_view_instrumentation` | -## Sidekiq Metrics available for Geo **(PREMIUM)** +## Sidekiq metrics -Sidekiq jobs may also gather metrics, and these metrics can be accessed if the Sidekiq exporter is enabled (e.g. via -the `monitoring.sidekiq_exporter` configuration option in `gitlab.yml`. +Sidekiq jobs may also gather metrics, and these metrics can be accessed if the +Sidekiq exporter is enabled (for example, using the `monitoring.sidekiq_exporter` +configuration option in `gitlab.yml`. These metrics are served from the +`/metrics` path on the configured port. | Metric | Type | Since | Description | Labels | |:---------------------------------------------- |:------- |:----- |:----------- |:------ | @@ -145,6 +153,7 @@ the `monitoring.sidekiq_exporter` configuration option in `gitlab.yml`. | `geo_repositories_checked_failed_count` | Gauge | 11.1 | Number of repositories that have a failure from `git fsck` | url | | `geo_repositories_retrying_verification_count` | Gauge | 11.2 | Number of repositories verification failures that Geo is actively trying to correct on secondary | url | | `geo_wikis_retrying_verification_count` | Gauge | 11.2 | Number of wikis verification failures that Geo is actively trying to correct on secondary | url | +| `global_search_bulk_cron_queue_size` | Gauge | 12.10 | Number of database records waiting to be synchronized to Elasticsearch | | ## Database load balancing metrics **(PREMIUM ONLY)** diff --git a/lib/gitlab/ci/pipeline/seed/build/cache.rb b/lib/gitlab/ci/pipeline/seed/build/cache.rb index 7671035b896..a4127ea0be2 100644 --- a/lib/gitlab/ci/pipeline/seed/build/cache.rb +++ b/lib/gitlab/ci/pipeline/seed/build/cache.rb @@ -51,8 +51,6 @@ module Gitlab end def hash_of_the_latest_changes - return unless Feature.enabled?(:ci_file_based_cache, @pipeline.project, default_enabled: true) - ids = files.map { |path| last_commit_id_for_path(path) } ids = ids.compact.sort.uniq diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb index aab10aef398..a072036daa8 100644 --- a/lib/gitlab/ci/variables/collection/item.rb +++ b/lib/gitlab/ci/variables/collection/item.rb @@ -37,7 +37,7 @@ module Gitlab case resource when Hash self.new(resource.symbolize_keys) - when ::HasVariable + when ::Ci::HasVariable self.new(resource.to_runner_variable) when self resource.dup diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js index a51846023ac..22aacdc735c 100644 --- a/spec/frontend/repository/components/table/row_spec.js +++ b/spec/frontend/repository/components/table/row_spec.js @@ -4,7 +4,10 @@ import { visitUrl } from '~/lib/utils/url_utility'; import TableRow from '~/repository/components/table/row.vue'; import Icon from '~/vue_shared/components/icon.vue'; -jest.mock('~/lib/utils/url_utility'); +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + visitUrl: jest.fn(), +})); let vm; let $router; diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index dec7d6b2df3..2631c219222 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -204,7 +204,6 @@ describe BlobHelper do end describe '#show_suggest_pipeline_creation_celebration?' do - let(:blob) { fake_blob(path: Gitlab::FileDetector::PATTERNS[:gitlab_ci]) } let(:current_user) { create(:user) } before do @@ -212,52 +211,68 @@ describe BlobHelper do assign(:blob, blob) assign(:commit, double('Commit', sha: 'whatever')) helper.request.cookies["suggest_gitlab_ci_yml_commit_#{project.id}"] = 'true' - allow(blob).to receive(:auxiliary_viewer).and_return(double('viewer', valid?: true)) allow(helper).to receive(:current_user).and_return(current_user) end - context 'experiment enabled' do - before do - allow(helper).to receive(:experiment_enabled?).and_return(true) - end - - it 'is true' do - expect(helper.show_suggest_pipeline_creation_celebration?).to be_truthy - end + context 'when file is a pipeline config file' do + let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) } + let(:blob) { fake_blob(path: Gitlab::FileDetector::PATTERNS[:gitlab_ci], data: data) } - context 'file is invalid format' do + context 'experiment enabled' do before do - allow(blob).to receive(:auxiliary_viewer).and_return(double('viewer', valid?: false)) + allow(helper).to receive(:experiment_enabled?).and_return(true) end - it 'is false' do - expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey + it 'is true' do + expect(helper.show_suggest_pipeline_creation_celebration?).to be_truthy end - end - context 'path is not a ci file' do - before do - allow(blob).to receive(:path).and_return('something_bad') + context 'file is invalid format' do + let(:data) { 'foo' } + + it 'is false' do + expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey + end end - it 'is false' do - expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey + context 'does not use the default ci config' do + before do + project.ci_config_path = 'something_bad' + end + + it 'is false' do + expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey + end + end + + context 'does not have the needed cookie' do + before do + helper.request.cookies.delete "suggest_gitlab_ci_yml_commit_#{project.id}" + end + + it 'is false' do + expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey + end end end - context 'does not use the default ci config' do + context 'experiment disabled' do before do - project.ci_config_path = 'something_bad' + allow(helper).to receive(:experiment_enabled?).and_return(false) end it 'is false' do expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey end end + end - context 'does not have the needed cookie' do + context 'when file is not a pipeline config file' do + let(:blob) { fake_blob(path: 'LICENSE') } + + context 'experiment enabled' do before do - helper.request.cookies.delete "suggest_gitlab_ci_yml_commit_#{project.id}" + allow(helper).to receive(:experiment_enabled?).and_return(true) end it 'is false' do @@ -265,16 +280,6 @@ describe BlobHelper do end end end - - context 'experiment disabled' do - before do - allow(helper).to receive(:experiment_enabled?).and_return(false) - end - - it 'is false' do - expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey - end - end end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb index 6a8b804597c..fe19244659f 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb @@ -83,16 +83,6 @@ describe Gitlab::Ci::Pipeline::Seed::Build::Cache do it_behaves_like 'version and gemfile files' end - context 'with feature flag disabled' do - let(:files) { ['VERSION', 'Gemfile.zip'] } - - before do - stub_feature_flags(ci_file_based_cache: false) - end - - it_behaves_like 'default key' - end - context 'with files ending with /' do let(:files) { ['Gemfile.zip/'] } diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb index 406a69f3bbc..610db9bf0e5 100644 --- a/spec/models/ci/group_variable_spec.rb +++ b/spec/models/ci/group_variable_spec.rb @@ -8,7 +8,7 @@ describe Ci::GroupVariable do it_behaves_like "CI variable" it { is_expected.to include_module(Presentable) } - it { is_expected.to include_module(Maskable) } + it { is_expected.to include_module(Ci::Maskable) } it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id).with_message(/\(\w+\) has already been taken/) } describe '.unprotected' do diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 3ff547456c6..810a0ddfd2e 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -9,7 +9,7 @@ describe Ci::Variable do describe 'validations' do it { is_expected.to include_module(Presentable) } - it { is_expected.to include_module(Maskable) } + it { is_expected.to include_module(Ci::Maskable) } it { is_expected.to include_module(HasEnvironmentScope) } it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope).with_message(/\(\w+\) has already been taken/) } end diff --git a/spec/models/concerns/has_variable_spec.rb b/spec/models/concerns/ci/has_variable_spec.rb index 2bb21d7934e..c132fe47c3c 100644 --- a/spec/models/concerns/has_variable_spec.rb +++ b/spec/models/concerns/ci/has_variable_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe HasVariable do +describe Ci::HasVariable do subject { build(:ci_variable) } it { is_expected.to validate_presence_of(:key) } diff --git a/spec/models/concerns/maskable_spec.rb b/spec/models/concerns/ci/maskable_spec.rb index aeba7ad862f..22ffb294819 100644 --- a/spec/models/concerns/maskable_spec.rb +++ b/spec/models/concerns/ci/maskable_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Maskable do +describe Ci::Maskable do let(:variable) { build(:ci_variable) } describe 'masked value validations' do @@ -34,7 +34,7 @@ describe Maskable do end describe 'REGEX' do - subject { Maskable::REGEX } + subject { Ci::Maskable::REGEX } it 'does not match strings shorter than 8 letters' do expect(subject.match?('hello')).to eq(false) diff --git a/spec/models/jira_import_data_spec.rb b/spec/models/jira_import_data_spec.rb new file mode 100644 index 00000000000..ad7a704236b --- /dev/null +++ b/spec/models/jira_import_data_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe JiraImportData do + let(:symbol_keys_project) do + { key: 'AA', scheduled_at: 2.days.ago.strftime('%Y-%m-%d %H:%M:%S'), scheduled_by: { 'user_id' => 1, 'name' => 'tester1' } } + end + + let(:string_keys_project) do + { 'key': 'BB', 'scheduled_at': 1.hour.ago.strftime('%Y-%m-%d %H:%M:%S'), 'scheduled_by': { 'user_id': 2, 'name': 'tester2' } } + end + + let(:jira_project_details) do + JiraImportData::JiraProjectDetails.new('CC', 1.day.ago.strftime('%Y-%m-%d %H:%M:%S'), { user_id: 3, name: 'tester3' }) + end + + describe '#projects' do + it 'returns empty array if no data' do + expect(described_class.new.projects).to eq([]) + end + + it 'returns empty array if no projects' do + import_data = described_class.new(data: { 'some-key' => 10 }) + expect(import_data.projects).to eq([]) + end + + it 'returns JiraProjectDetails sorted by scheduled_at time' do + import_data = described_class.new(data: { jira: { projects: [symbol_keys_project, string_keys_project, jira_project_details] } }) + + expect(import_data.projects.size).to eq 3 + expect(import_data.projects.map(&:key)).to eq(%w(AA CC BB)) + expect(import_data.projects.map(&:scheduled_by).map {|e| e['name']}).to eq %w(tester1 tester3 tester2) + expect(import_data.projects.map(&:scheduled_by).map {|e| e['user_id']}).to eq [1, 3, 2] + end + end + + describe 'add projects' do + it 'adds project when data is nil' do + import_data = described_class.new + expect(import_data.data).to be nil + + import_data << string_keys_project + + expect(import_data.data).to eq({ 'jira' => { 'projects' => [string_keys_project] } }) + end + + it 'adds project when data has some random info' do + import_data = described_class.new(data: { 'one-key': 10 }) + expect(import_data.data).to eq({ 'one-key' => 10 }) + + import_data << string_keys_project + + expect(import_data.data).to eq({ 'one-key' => 10, 'jira' => { 'projects' => [string_keys_project] } }) + end + + it 'adds project when data already has some jira projects' do + import_data = described_class.new(data: { jira: { projects: [symbol_keys_project] } }) + expect(import_data.projects.map(&:to_h)).to eq [symbol_keys_project] + + import_data << string_keys_project + + expect(import_data.data['jira']['projects'].size).to eq 2 + expect(import_data.projects.map(&:key)).to eq(%w(AA BB)) + expect(import_data.projects.map(&:scheduled_by).map {|e| e['name']}).to eq %w(tester1 tester2) + expect(import_data.projects.map(&:scheduled_by).map {|e| e['user_id']}).to eq [1, 2] + end + end + + describe '#force_import!' do + it 'sets force import when data is nil' do + import_data = described_class.new + + import_data.force_import! + + expect(import_data.data['jira'][JiraImportData::FORCE_IMPORT_KEY]).to be true + expect(import_data.force_import?).to be false + end + + it 'sets force import when data is present but no jira key' do + import_data = described_class.new(data: { 'some-key': 'some-data' }) + + import_data.force_import! + + expect(import_data.data['jira'][JiraImportData::FORCE_IMPORT_KEY]).to be true + expect(import_data.data).to eq({ 'some-key' => 'some-data', 'jira' => { JiraImportData::FORCE_IMPORT_KEY => true } }) + expect(import_data.force_import?).to be false + end + + it 'sets force import when data and jira keys exist' do + import_data = described_class.new(data: { 'some-key': 'some-data', 'jira': {} }) + + import_data.force_import! + + expect(import_data.data['jira'][JiraImportData::FORCE_IMPORT_KEY]).to be true + expect(import_data.data).to eq({ 'some-key' => 'some-data', 'jira' => { JiraImportData::FORCE_IMPORT_KEY => true } }) + expect(import_data.force_import?).to be false + end + + it 'sets force import when data and jira project data exist' do + import_data = described_class.new(data: { jira: { projects: [symbol_keys_project], JiraImportData::FORCE_IMPORT_KEY => false }, 'some-key': 'some-data' }) + + import_data.force_import! + + expect(import_data.data['jira'][JiraImportData::FORCE_IMPORT_KEY]).to be true + expect(import_data.data).to eq({ 'some-key' => 'some-data', 'jira' => { 'projects' => [symbol_keys_project.deep_stringify_keys!], JiraImportData::FORCE_IMPORT_KEY => true } }) + expect(import_data.force_import?).to be true + end + end + + describe '#force_import?' do + it 'returns false when data blank' do + expect(described_class.new.force_import?).to be false + end + + it 'returns false if there is no project data present' do + import_data = described_class.new(data: { jira: { JiraImportData::FORCE_IMPORT_KEY => true }, 'one-key': 10 }) + + expect(import_data.force_import?).to be false + end + + it 'returns false when force import set to false' do + import_data = described_class.new(data: { jira: { projects: [symbol_keys_project], JiraImportData::FORCE_IMPORT_KEY => false }, 'one-key': 10 }) + + expect(import_data.force_import?).to be false + end + + it 'returns true when force import set to true' do + import_data = described_class.new(data: { jira: { projects: [symbol_keys_project], JiraImportData::FORCE_IMPORT_KEY => true } }) + + expect(import_data.force_import?).to be true + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ae97e5340e2..15b409b2dcf 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2353,6 +2353,63 @@ describe Project do expect(project.add_import_job).to eq(import_jid) end end + + context 'jira import' do + it 'schedules a jira import job' do + project = create(:project, import_type: 'jira') + + expect(Gitlab::JiraImport::Stage::StartImportWorker).to receive(:perform_async).with(project.id).and_return(import_jid) + expect(project.add_import_job).to eq(import_jid) + end + end + end + + describe '#jira_import?' do + subject(:project) { build(:project, import_type: 'jira') } + + it { expect(project.jira_import?).to be true } + it { expect(project.import?).to be true } + end + + describe '#jira_force_import?' do + let(:imported_jira_project) do + JiraImportData::JiraProjectDetails.new('xx', Time.now.strftime('%Y-%m-%d %H:%M:%S'), { user_id: 1, name: 'root' }) + end + let(:jira_import_data) do + data = JiraImportData.new + data << imported_jira_project + data.force_import! + data + end + + subject(:project) { build(:project, import_type: 'jira', import_data: jira_import_data) } + + it { expect(project.jira_force_import?).to be true } + end + + describe '#remove_import_data' do + let(:import_data) { ProjectImportData.new(data: { 'test' => 'some data' }) } + + context 'when jira import' do + let!(:project) { create(:project, import_type: 'jira', import_data: import_data) } + + it 'does not remove import data' do + expect(project.mirror?).to be false + expect(project.jira_import?).to be true + expect { project.remove_import_data }.not_to change { ProjectImportData.count } + end + end + + context 'when not mirror neither jira import' do + let(:user) { create(:user) } + let!(:project) { create(:project, import_type: 'github', import_data: import_data) } + + it 'removes import data' do + expect(project.mirror?).to be false + expect(project.jira_import?).to be false + expect { project.remove_import_data }.to change { ProjectImportData.count }.by(-1) + end + end end describe '#gitlab_project_import?' do diff --git a/spec/services/milestones/transfer_service_spec.rb b/spec/services/milestones/transfer_service_spec.rb index 9b087b07cea..9f94d2d320b 100644 --- a/spec/services/milestones/transfer_service_spec.rb +++ b/spec/services/milestones/transfer_service_spec.rb @@ -40,6 +40,16 @@ describe Milestones::TransferService do expect(new_milestone.project_milestone?).to be_truthy end + context 'when milestone is from an ancestor group' do + let(:old_group_ancestor) { create(:group) } + let(:old_group) { create(:group, parent: old_group_ancestor) } + let(:group_milestone) { create(:milestone, group: old_group_ancestor)} + + it 'recreates the missing group milestones at project level' do + expect { service.execute }.to change(project.milestones, :count).by(1) + end + end + it 'deletes milestone issue counters cache for both milestones' do new_milestone = create(:milestone, project: project, title: group_milestone.title) diff --git a/spec/support/shared_examples/models/ci_variable_shared_examples.rb b/spec/support/shared_examples/models/ci_variable_shared_examples.rb index 6cc922b4101..e5463f26369 100644 --- a/spec/support/shared_examples/models/ci_variable_shared_examples.rb +++ b/spec/support/shared_examples/models/ci_variable_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'CI variable' do - it { is_expected.to include_module(HasVariable) } + it { is_expected.to include_module(Ci::HasVariable) } describe "variable type" do it 'defines variable types' do diff --git a/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb b/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb new file mode 100644 index 00000000000..5448526f954 --- /dev/null +++ b/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +shared_examples 'include import workers modules' do + it { expect(described_class).to include_module(ApplicationWorker) } + it { expect(described_class).to include_module(Gitlab::JiraImport::QueueOptions) } + + if described_class == Gitlab::JiraImport::Stage::StartImportWorker + it { expect(described_class).to include_module(ProjectStartImport) } + it { expect(described_class).to include_module(ProjectImportOptions) } + else + it { expect(described_class).to include_module(Gitlab::JiraImport::ImportWorker) } + end +end + +shared_examples 'exit import not started' do + it 'does nothing, and exits' do + expect(Gitlab::JiraImport::AdvanceStageWorker).not_to receive(:perform_async) + + worker.perform(project.id) + end +end + +shared_examples 'advance to next stage' do |next_stage| + let(:job_waiter) { Gitlab::JobWaiter.new(2, 'some-job-key') } + + it "advances to #{next_stage} stage" do + expect(Gitlab::JobWaiter).to receive(:new).and_return(job_waiter) + expect(Gitlab::JiraImport::AdvanceStageWorker).to receive(:perform_async).with(project.id, { job_waiter.key => job_waiter.jobs_remaining }, next_stage.to_sym) + + worker.perform(project.id) + end +end diff --git a/spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb new file mode 100644 index 00000000000..fa0c7d83851 --- /dev/null +++ b/spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::JiraImport::Stage::FinishImportWorker do + let(:project) { create(:project) } + let(:worker) { described_class.new } + + describe 'modules' do + it_behaves_like 'include import workers modules' + end + + describe '#perform' do + context 'when feature flag enabled' do + before do + stub_feature_flags(jira_issue_import: false) + end + + it_behaves_like 'exit import not started' + end + + context 'when feature flag enabled' do + before do + stub_feature_flags(jira_issue_import: true) + end + + context 'when import did not start' do + let!(:import_state) { create(:import_state, project: project) } + + it_behaves_like 'exit import not started' + end + + context 'when import started' do + let(:imported_jira_project) do + JiraImportData::JiraProjectDetails.new('xx', Time.now.strftime('%Y-%m-%d %H:%M:%S'), { user_id: 1, name: 'root' }) + end + let(:jira_import_data) do + data = JiraImportData.new + data << imported_jira_project + data.force_import! + data + end + let(:import_state) { create(:import_state, status: :started) } + let(:project) { create(:project, import_type: 'jira', import_data: jira_import_data, import_state: import_state) } + + it 'changes import state to finished' do + worker.perform(project.id) + + expect(project.reload.import_state.status).to eq "finished" + end + + it 'removes force-import flag' do + expect(project.reload.import_data.data['jira'][JiraImportData::FORCE_IMPORT_KEY]).to be true + + worker.perform(project.id) + + expect(project.reload.import_data.data['jira'][JiraImportData::FORCE_IMPORT_KEY]).to be nil + expect(project.reload.import_data.data['jira']).not_to be nil + end + end + end + end +end diff --git a/spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb new file mode 100644 index 00000000000..fa2f3501973 --- /dev/null +++ b/spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::JiraImport::Stage::ImportAttachmentsWorker do + let(:project) { create(:project) } + let(:worker) { described_class.new } + + describe 'modules' do + it_behaves_like 'include import workers modules' + end + + describe '#perform' do + context 'when feature flag enabled' do + before do + stub_feature_flags(jira_issue_import: false) + end + + it_behaves_like 'exit import not started' + end + + context 'when feature flag enabled' do + before do + stub_feature_flags(jira_issue_import: true) + end + + context 'when import did not start' do + let!(:import_state) { create(:import_state, project: project) } + + it_behaves_like 'exit import not started' + end + + context 'when import started' do + let!(:import_state) { create(:import_state, status: :started, project: project) } + + it_behaves_like 'advance to next stage', :notes + end + end + end +end diff --git a/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb new file mode 100644 index 00000000000..b43519a3e5d --- /dev/null +++ b/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::JiraImport::Stage::ImportIssuesWorker do + let(:project) { create(:project) } + let(:worker) { described_class.new } + + describe 'modules' do + it_behaves_like 'include import workers modules' + end + + describe '#perform' do + context 'when feature flag enabled' do + before do + stub_feature_flags(jira_issue_import: false) + end + + it_behaves_like 'exit import not started' + end + + context 'when feature flag enabled' do + before do + stub_feature_flags(jira_issue_import: true) + end + + context 'when import did not start' do + let!(:import_state) { create(:import_state, project: project) } + + it_behaves_like 'exit import not started' + end + + context 'when import started' do + let!(:import_state) { create(:import_state, status: :started, project: project) } + + it_behaves_like 'advance to next stage', :attachments + end + end + end +end diff --git a/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb new file mode 100644 index 00000000000..827efb85a17 --- /dev/null +++ b/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::JiraImport::Stage::ImportLabelsWorker do + let(:project) { create(:project) } + let(:worker) { described_class.new } + + describe 'modules' do + it_behaves_like 'include import workers modules' + end + + describe '#perform' do + context 'when feature flag enabled' do + before do + stub_feature_flags(jira_issue_import: false) + end + + it_behaves_like 'exit import not started' + end + + context 'when feature flag enabled' do + before do + stub_feature_flags(jira_issue_import: true) + end + + context 'when import did not start' do + let!(:import_state) { create(:import_state, project: project) } + + it_behaves_like 'exit import not started' + end + + context 'when import started' do + let!(:import_state) { create(:import_state, status: :started, project: project) } + + it_behaves_like 'advance to next stage', :issues + end + end + end +end diff --git a/spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb new file mode 100644 index 00000000000..bd6b36613cc --- /dev/null +++ b/spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::JiraImport::Stage::ImportNotesWorker do + let(:project) { create(:project) } + let(:worker) { described_class.new } + + describe 'modules' do + it_behaves_like 'include import workers modules' + end + + describe '#perform' do + context 'when feature flag enabled' do + before do + stub_feature_flags(jira_issue_import: false) + end + + it_behaves_like 'exit import not started' + end + + context 'when feature flag enabled' do + before do + stub_feature_flags(jira_issue_import: true) + end + + context 'when import did not start' do + let!(:import_state) { create(:import_state, project: project) } + + it_behaves_like 'exit import not started' + end + + context 'when import started' do + let!(:import_state) { create(:import_state, status: :started, project: project) } + + it_behaves_like 'advance to next stage', :finish + end + end + end +end diff --git a/spec/workers/gitlab/jira_import/stage/start_import_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/start_import_worker_spec.rb new file mode 100644 index 00000000000..cc70277384d --- /dev/null +++ b/spec/workers/gitlab/jira_import/stage/start_import_worker_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::JiraImport::Stage::StartImportWorker do + let(:project) { create(:project) } + let(:worker) { described_class.new } + let(:jid) { '12345678' } + + describe 'modules' do + it_behaves_like 'include import workers modules' + end + + describe '#perform' do + context 'when feature flag not enabled' do + before do + stub_feature_flags(jira_issue_import: false) + end + + it 'exits because import not allowed' do + expect(Gitlab::JiraImport::Stage::ImportLabelsWorker).not_to receive(:perform_async) + + worker.perform(project.id) + end + end + + context 'when feature flag not enabled' do + before do + stub_feature_flags(jira_issue_import: true) + end + + context 'when import is not scheudled' do + let!(:import_state) { create(:import_state, project: project, status: :none, jid: jid) } + + it 'exits because import not started' do + expect(Gitlab::JiraImport::Stage::ImportLabelsWorker).not_to receive(:perform_async) + + worker.perform(project.id) + end + end + + context 'when import is scheduled' do + let!(:import_state) { create(:import_state, project: project, status: :scheduled, jid: jid) } + + it 'advances to importing labels' do + expect(Gitlab::JiraImport::Stage::ImportLabelsWorker).to receive(:perform_async) + + worker.perform(project.id) + end + end + + context 'when import is started' do + let!(:import_state) { create(:import_state, project: project, status: :started, jid: jid) } + + context 'when this is the same worker that stated import' do + it 'advances to importing labels' do + allow(worker).to receive(:jid).and_return(jid) + expect(Gitlab::JiraImport::Stage::ImportLabelsWorker).to receive(:perform_async) + + worker.perform(project.id) + end + end + + context 'when this is a different worker that stated import' do + it 'advances to importing labels' do + allow(worker).to receive(:jid).and_return('87654321') + expect(Gitlab::JiraImport::Stage::ImportLabelsWorker).not_to receive(:perform_async) + + worker.perform(project.id) + end + end + end + + context 'when import is finished' do + let!(:import_state) { create(:import_state, project: project, status: :finished, jid: jid) } + + it 'advances to importing labels' do + allow(worker).to receive(:jid).and_return(jid) + expect(Gitlab::JiraImport::Stage::ImportLabelsWorker).not_to receive(:perform_async) + + worker.perform(project.id) + end + end + end + end +end |