Read about our upcoming Code of Conduct on this issue

This instance will be upgraded to Heptapod 0.28.1 on 2022-01-26 at 16:00 UTC+1 (a few minutes of down time)

Commit 900d7e76 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent ac825847aa1e
......@@ -20,12 +20,11 @@ build-qa-image:
- time docker build --cache-from "${QA_MASTER_IMAGE}" --tag ${QA_IMAGE} --file ./qa/Dockerfile ./
- time docker push ${QA_IMAGE}
review-cleanup:
.review-cleanup-base:
extends:
- .default-retry
- .review:rules:review-cleanup
stage: prepare
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
allow_failure: true
environment:
name: review/auto-cleanup
......@@ -36,6 +35,18 @@ review-cleanup:
script:
- ruby -rrubygems scripts/review_apps/automated_cleanup.rb
review-cleanup:
extends:
- .review-cleanup-base
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
review-cleanup-helm3:
extends:
- .review-cleanup-base
variables:
HELM_3: 1
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-helm3-kubectl1.14
review-gcp-cleanup:
extends:
- .review:rules:review-gcp-cleanup
......
......@@ -2,6 +2,7 @@
/* eslint-disable vue/require-default-prop */
import IssueCardInner from './issue_card_inner.vue';
import eventHub from '../eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
import boardsStore from '../stores/boards_store';
export default {
......@@ -73,6 +74,11 @@ export default {
showIssue(e) {
if (e.target.classList.contains('js-no-trigger')) return;
// If no issues are opened, close all sidebars first
if (!boardsStore.detail?.issue?.id) {
sidebarEventHub.$emit('sidebar.closeAll');
}
// If CMD or CTRL is clicked
const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey);
......
......@@ -103,12 +103,14 @@ export default Vue.extend({
eventHub.$on('sidebar.addAssignee', this.addAssignee);
eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
eventHub.$on('sidebar.closeAll', this.closeSidebar);
},
beforeDestroy() {
eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
eventHub.$off('sidebar.addAssignee', this.addAssignee);
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
eventHub.$off('sidebar.closeAll', this.closeSidebar);
},
mounted() {
new IssuableContext(this.currentUser);
......
......@@ -6,18 +6,20 @@ class GroupsController < Groups::ApplicationController
include ParamsBackwardCompatibility
include PreviewMarkdown
include RecordUserLastActivity
include SendFileUpload
extend ::Gitlab::Utils::Override
respond_to :html
prepend_before_action(only: [:show, :issues]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) }
prepend_before_action :ensure_export_enabled, only: [:export, :download_export]
before_action :authenticate_user!, only: [:new, :create]
before_action :group, except: [:index, :new, :create]
# Authorize
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects, :transfer]
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects, :transfer, :export, :download_export]
before_action :authorize_create_group!, only: [:new]
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
......@@ -29,6 +31,8 @@ class GroupsController < Groups::ApplicationController
push_frontend_feature_flag(:vue_issuables_list, @group)
end
before_action :export_rate_limit, only: [:export, :download_export]
skip_cross_project_access_check :index, :new, :create, :edit, :update,
:destroy, :projects
# When loading show as an atom feed, we render events that could leak cross
......@@ -134,6 +138,25 @@ def transfer
end
# rubocop: enable CodeReuse/ActiveRecord
def export
export_service = Groups::ImportExport::ExportService.new(group: @group, user: current_user)
if export_service.async_execute
redirect_to edit_group_path(@group), notice: _('Group export started.')
else
redirect_to edit_group_path(@group), alert: _('Group export could not be started.')
end
end
def download_export
if @group.export_file_exists?
send_upload(@group.export_file, attachment: @group.export_file.filename)
else
redirect_to edit_group_path(@group),
alert: _('Group export link has expired. Please generate a new export from your group settings.')
end
end
protected
def render_show_html
......@@ -234,6 +257,21 @@ def build_canonical_path(group)
url_for(safe_params)
end
def export_rate_limit
prefixed_action = "group_#{params[:action]}".to_sym
if Gitlab::ApplicationRateLimiter.throttled?(prefixed_action, scope: [current_user, prefixed_action, @group])
Gitlab::ApplicationRateLimiter.log_request(request, "#{prefixed_action}_request_limit".to_sym, current_user)
flash[:alert] = _('This endpoint has been requested too many times. Try again later.')
redirect_to edit_group_path(@group)
end
end
def ensure_export_enabled
render_404 unless Feature.enabled?(:group_import_export, @group, default_enabled: true)
end
private
def groups
......
......@@ -9,7 +9,7 @@ class JiraController < Projects::ApplicationController
def show
return if Feature.enabled?(:jira_issue_import_vue, @project)
unless @project.import_state&.in_progress?
unless @project.latest_jira_import&.in_progress?
jira_client = @project.jira_service.client
jira_projects = jira_client.Project.all
......@@ -20,7 +20,7 @@ def show
end
end
flash[:notice] = _("Import %{status}") % { status: @project.import_state.status } if @project.import_state.present? && !@project.import_state.none?
flash[:notice] = _("Import %{status}") % { status: @project.jira_import_status } unless @project.latest_jira_import&.initial?
end
def import
......
......@@ -30,11 +30,11 @@ def resolve(project_path:, jira_project_key:)
service_response = ::JiraImport::StartImportService
.new(context[:current_user], project, jira_project_key)
.execute
import_data = service_response.payload[:import_data]
jira_import = service_response.success? ? service_response.payload[:import_data] : nil
errors = service_response.error? ? [service_response.message] : []
{
jira_import: import_data.errors.blank? ? import_data.projects.last : nil,
errors: errors_on_object(import_data)
jira_import: jira_import,
errors: errors
}
end
......
......@@ -8,11 +8,9 @@ class JiraImportsResolver < BaseResolver
alias_method :project, :object
def resolve(**args)
return JiraImportData.none unless project&.import_data.present?
authorize!(project)
project.import_data.becomes(JiraImportData).projects
project.jira_imports
end
def authorized_resource?(project)
......
......@@ -8,20 +8,12 @@ class JiraImportType < BaseObject
graphql_name 'JiraImport'
field :scheduled_at, Types::TimeType, null: true,
description: 'Timestamp of when the Jira import was created/started'
method: :created_at,
description: 'Timestamp of when the Jira import was created'
field :scheduled_by, Types::UserType, null: true,
description: 'User that started the Jira import'
field :jira_project_key, GraphQL::STRING_TYPE, null: false,
description: 'Project key for the imported Jira project',
method: :key
def scheduled_at
DateTime.parse(object.scheduled_at)
end
def scheduled_by
::Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.scheduled_by['user_id']).find
end
description: 'Project key for the imported Jira project'
end
# rubocop: enable Graphql/AuthorizeTypes
end
......@@ -16,7 +16,7 @@ def refresh_jid_expiration
end
def self.jid_by(project_id:, status:)
select(:jid).with_status(status).find_by(project_id: project_id)
select(:jid).where(status: status).find_by(project_id: project_id)
end
end
end
......
# frozen_string_literal: true
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|
JiraProjectDetails.new(p['key'], p['scheduled_at'], p['scheduled_by'])
end
projects&.sort_by { |jp| jp.scheduled_at } || []
end
def <<(project)
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
def current_project
projects.last
end
end
......@@ -22,6 +22,8 @@ class JiraImportState < ApplicationRecord
message: _('Cannot have multiple Jira imports running at the same time')
}
alias_method :scheduled_by, :user
state_machine :status, initial: :initial do
event :schedule do
transition initial: :scheduled
......@@ -46,6 +48,11 @@ class JiraImportState < ApplicationRecord
end
end
before_transition any => :finished do |state, _|
InternalId.flush_records!(project: state.project)
state.project.update_project_counter_caches
end
after_transition any => :finished do |state, _|
if state.jid.present?
Gitlab::SidekiqStatus.unset(state.jid)
......@@ -67,4 +74,8 @@ def owner_class_attribute_default
def in_progress?
scheduled? || started?
end
def non_initial?
!initial?
end
end
......@@ -859,9 +859,7 @@ def import_status
end
def jira_import_status
return import_status if jira_force_import?
import_data&.becomes(JiraImportData)&.projects.blank? ? 'none' : 'finished'
latest_jira_import&.status || 'initial'
end
def human_import_status_name
......@@ -875,8 +873,6 @@ def add_import_job
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
......@@ -909,7 +905,7 @@ def reset_cache_and_import_attrs
# This method is overridden in EE::Project model
def remove_import_data
import_data&.destroy unless jira_import?
import_data&.destroy
end
def ci_config_path=(value)
......@@ -972,11 +968,7 @@ def bare_repository_import?
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?
import_type == 'jira' && latest_jira_import.present? && Feature.enabled?(:jira_issue_import, self)
end
def gitlab_project_import?
......
......@@ -10,9 +10,15 @@ def initialize(group:, user:, params: {})
@shared = @params[:shared] || Gitlab::ImportExport::Shared.new(@group)
end
def async_execute
GroupExportWorker.perform_async(@current_user.id, @group.id, @params)
end
def execute
validate_user_permissions
remove_existing_export! if @group.export_file_exists?
save!
ensure
cleanup
......@@ -30,6 +36,13 @@ def validate_user_permissions
end
end
def remove_existing_export!
import_export_upload = @group.import_export_upload
import_export_upload.remove_export_file!
import_export_upload.save
end
def save!
if savers.all?(&:save)
notify_success
......
......@@ -20,23 +20,27 @@ def execute
private
def create_and_schedule_import
import_data = project.create_or_update_import_data(data: {}).becomes(JiraImportData)
jira_project_details = JiraImportData::JiraProjectDetails.new(
jira_project_key,
Time.now.strftime('%Y-%m-%d %H:%M:%S'),
{ user_id: user.id, name: user.name }
)
import_data << jira_project_details
import_data.force_import!
jira_import = build_jira_import
project.import_type = 'jira'
project.import_state.schedule if project.save!
project.save! && jira_import.schedule!
ServiceResponse.success(payload: { import_data: import_data } )
ServiceResponse.success(payload: { import_data: jira_import } )
rescue => ex
# in case project.save! raises an erorr
Gitlab::ErrorTracking.track_exception(ex, project_id: project.id)
build_error_response(ex.message)
jira_import.do_fail!
end
def build_jira_import
project.jira_imports.build(
user: user,
jira_project_key: jira_project_key,
# we do not have the jira_project_name or jira_project_xid yet so just set a mock value,
# we will once https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28190
jira_project_name: jira_project_key,
jira_project_xid: 0
)
end
def validate
......@@ -48,18 +52,11 @@ def validate
end
def build_error_response(message)
import_data = JiraImportData.new(project: project)
import_data.errors.add(:base, message)
ServiceResponse.error(
message: import_data.errors.full_messages.to_sentence,
http_status: 400,
payload: { import_data: import_data }
)
ServiceResponse.error(message: message, http_status: 400)
end
def import_in_progress?
import_state = project.import_state || project.create_import_state
import_state.in_progress?
project.latest_jira_import&.in_progress?
end
end
end
......@@ -30,7 +30,7 @@
= _('Groups')
- if dashboard_nav_link?(:activity)
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity' do
= _('Activity')
- if dashboard_nav_link?(:milestones)
......
......@@ -28,7 +28,7 @@ def can_import?(project)
return false unless project
return false if Feature.disabled?(:jira_issue_import, project)
project.import_state.started?
project.latest_jira_import&.started?
end
end
end
......
......@@ -17,7 +17,7 @@ class AdvanceStageWorker # rubocop:disable Scalability/IdempotentWorker
}.freeze
def find_import_state(project_id)
ProjectImportState.jid_by(project_id: project_id, status: :started)
JiraImportState.jid_by(project_id: project_id, status: :started)
end
private
......
......@@ -9,11 +9,8 @@ class FinishImportWorker # rubocop:disable Scalability/IdempotentWorker
private
def import(project)
project.after_import
ensure
JiraImport.cache_cleanup(project.id)
project.import_data.becomes(JiraImportData).finish_import!
project.import_data.save!
project.latest_jira_import&.finish!
end
end
end
......
......@@ -13,7 +13,7 @@ def import(project)
# 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
project.latest_jira_import.refresh_jid_expiration
Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { fake_waiter.key => fake_waiter.jobs_remaining }, :notes)
end
end
......
......@@ -11,7 +11,7 @@ class ImportIssuesWorker # rubocop:disable Scalability/IdempotentWorker
def import(project)
jobs_waiter = Gitlab::JiraImport::IssuesImporter.new(project).execute
project.import_state.refresh_jid_expiration
project.latest_jira_import.refresh_jid_expiration
Gitlab::JiraImport::AdvanceStageWorker.perform_async(
project.id,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment