diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/sidebar/components/assignees/assignee_title.vue | 6 | ||||
-rw-r--r-- | app/models/clusters/applications/elastic_stack.rb | 3 | ||||
-rw-r--r-- | app/models/issue.rb | 2 | ||||
-rw-r--r-- | app/models/milestone.rb | 17 | ||||
-rw-r--r-- | app/services/projects/destroy_service.rb | 19 | ||||
-rw-r--r-- | app/services/repositories/base_service.rb | 8 | ||||
-rw-r--r-- | app/services/repositories/destroy_service.rb | 4 | ||||
-rw-r--r-- | app/services/snippets/bulk_destroy_service.rb | 74 | ||||
-rw-r--r-- | app/services/snippets/destroy_service.rb | 30 | ||||
-rw-r--r-- | app/services/users/destroy_service.rb | 5 | ||||
-rw-r--r-- | app/views/shared/issuable/_sidebar_assignees.html.haml | 2 |
11 files changed, 142 insertions, 28 deletions
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue index f4dac38b9e1..5c67e429383 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -1,8 +1,12 @@ <script> +import { GlLoadingIcon } from '@gitlab/ui'; import { n__ } from '~/locale'; export default { name: 'AssigneeTitle', + components: { + GlLoadingIcon, + }, props: { loading: { type: Boolean, @@ -34,7 +38,7 @@ export default { <template> <div class="title hide-collapsed"> {{ assigneeTitle }} - <i v-if="loading" aria-hidden="true" class="fa fa-spinner fa-spin block-loading"></i> + <gl-loading-icon v-if="loading" inline class="align-bottom" /> <a v-if="editable" class="js-sidebar-dropdown-toggle edit-link float-right" diff --git a/app/models/clusters/applications/elastic_stack.rb b/app/models/clusters/applications/elastic_stack.rb index ce42bc65579..f87d4e8ed49 100644 --- a/app/models/clusters/applications/elastic_stack.rb +++ b/app/models/clusters/applications/elastic_stack.rb @@ -15,9 +15,6 @@ module Clusters include ::Clusters::Concerns::ApplicationData include ::Gitlab::Utils::StrongMemoize - include IgnorableColumns - ignore_column :kibana_hostname, remove_with: '12.9', remove_after: '2020-02-22' - default_value_for :version, VERSION def chart diff --git a/app/models/issue.rb b/app/models/issue.rb index 1c191064d1a..f265b72f11f 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -44,6 +44,8 @@ class Issue < ApplicationRecord has_many :assignees, class_name: "User", through: :issue_assignees has_many :zoom_meetings has_many :user_mentions, class_name: "IssueUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + has_many :sent_notifications, as: :noteable + has_one :sentry_issue accepts_nested_attributes_for :sentry_issue diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 29c621c54d0..4ccfe314526 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -3,7 +3,13 @@ class Milestone < ApplicationRecord # Represents a "No Milestone" state used for filtering Issues and Merge # Requests that have no milestone assigned. - MilestoneStruct = Struct.new(:title, :name, :id) + MilestoneStruct = Struct.new(:title, :name, :id) do + # Ensure these models match the interface required for exporting + def serializable_hash(_opts = {}) + { title: title, name: name, id: id } + end + end + None = MilestoneStruct.new('No Milestone', 'No Milestone', 0) Any = MilestoneStruct.new('Any Milestone', '', -1) Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2) @@ -128,11 +134,12 @@ class Milestone < ApplicationRecord reorder(nil).group(:state).count end + def predefined_id?(id) + [Any.id, None.id, Upcoming.id, Started.id].include?(id) + end + def predefined?(milestone) - milestone == Any || - milestone == None || - milestone == Upcoming || - milestone == Started + predefined_id?(milestone&.id) end end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 066d1f1ca72..fd1366d2c4a 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -47,7 +47,7 @@ module Projects private - def trash_repositories! + def trash_project_repositories! unless remove_repository(project.repository) raise_error(s_('DeleteProject|Failed to remove project repository. Please try again or contact administrator.')) end @@ -57,6 +57,18 @@ module Projects end end + def trash_relation_repositories! + unless remove_snippets + raise_error(s_('DeleteProject|Failed to remove project snippets. Please try again or contact administrator.')) + end + end + + def remove_snippets + response = Snippets::BulkDestroyService.new(current_user, project.snippets).execute + + response.success? + end + def remove_repository(repository) return true unless repository @@ -95,7 +107,8 @@ module Projects Project.transaction do log_destroy_event - trash_repositories! + trash_relation_repositories! + trash_project_repositories! # Rails attempts to load all related records into memory before # destroying: https://github.com/rails/rails/issues/22510 @@ -103,7 +116,7 @@ module Projects # # Exclude container repositories because its before_destroy would be # called multiple times, and it doesn't destroy any database records. - project.destroy_dependent_associations_in_batches(exclude: [:container_repositories]) + project.destroy_dependent_associations_in_batches(exclude: [:container_repositories, :snippets]) project.destroy! end end diff --git a/app/services/repositories/base_service.rb b/app/services/repositories/base_service.rb index 6a39399c791..a99a65b7edb 100644 --- a/app/services/repositories/base_service.rb +++ b/app/services/repositories/base_service.rb @@ -7,8 +7,8 @@ class Repositories::BaseService < BaseService attr_reader :repository - delegate :project, :disk_path, :full_path, to: :repository - delegate :repository_storage, to: :project + delegate :container, :disk_path, :full_path, to: :repository + delegate :repository_storage, to: :container def initialize(repository) @repository = repository @@ -31,7 +31,7 @@ class Repositories::BaseService < BaseService # gitlab/cookies.git -> gitlab/cookies+119+deleted.git # def removal_path - "#{disk_path}+#{project.id}#{DELETED_FLAG}" + "#{disk_path}+#{container.id}#{DELETED_FLAG}" end # If we get a Gitaly error, the repository may be corrupted. We can @@ -40,7 +40,7 @@ class Repositories::BaseService < BaseService def ignore_git_errors(&block) yield rescue Gitlab::Git::CommandError => e - Gitlab::GitLogger.warn(class: self.class.name, project_id: project.id, disk_path: disk_path, message: e.to_s) + Gitlab::GitLogger.warn(class: self.class.name, container_id: container.id, disk_path: disk_path, message: e.to_s) end def move_error(path) diff --git a/app/services/repositories/destroy_service.rb b/app/services/repositories/destroy_service.rb index 374968f610e..b12d0744387 100644 --- a/app/services/repositories/destroy_service.rb +++ b/app/services/repositories/destroy_service.rb @@ -14,11 +14,11 @@ class Repositories::DestroyService < Repositories::BaseService log_info(%Q{Repository "#{disk_path}" moved to "#{removal_path}" for repository "#{full_path}"}) current_repository = repository - project.run_after_commit do + container.run_after_commit do Repositories::ShellDestroyService.new(current_repository).execute end - log_info("Project \"#{project.full_path}\" was removed") + log_info("Repository \"#{full_path}\" was removed") success else diff --git a/app/services/snippets/bulk_destroy_service.rb b/app/services/snippets/bulk_destroy_service.rb new file mode 100644 index 00000000000..d9cc383a5a6 --- /dev/null +++ b/app/services/snippets/bulk_destroy_service.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Snippets + class BulkDestroyService + include Gitlab::Allowable + + attr_reader :current_user, :snippets + + DeleteRepositoryError = Class.new(StandardError) + SnippetAccessError = Class.new(StandardError) + + def initialize(user, snippets) + @current_user = user + @snippets = snippets + end + + def execute + return ServiceResponse.success(message: 'No snippets found.') if snippets.empty? + + user_can_delete_snippets! + attempt_delete_repositories! + snippets.destroy_all # rubocop: disable DestroyAll + + ServiceResponse.success(message: 'Snippets were deleted.') + rescue SnippetAccessError + service_response_error("You don't have access to delete these snippets.", 403) + rescue DeleteRepositoryError + attempt_rollback_repositories + service_response_error('Failed to delete snippet repositories.', 400) + rescue + # In case the delete operation fails + attempt_rollback_repositories + service_response_error('Failed to remove snippets.', 400) + end + + private + + def user_can_delete_snippets! + allowed = DeclarativePolicy.user_scope do + snippets.find_each.all? { |snippet| user_can_delete_snippet?(snippet) } + end + + raise SnippetAccessError unless allowed + end + + def user_can_delete_snippet?(snippet) + can?(current_user, :admin_snippet, snippet) + end + + def attempt_delete_repositories! + snippets.each do |snippet| + result = Repositories::DestroyService.new(snippet.repository).execute + + raise DeleteRepositoryError if result[:status] == :error + end + end + + def attempt_rollback_repositories + snippets.each do |snippet| + result = Repositories::DestroyRollbackService.new(snippet.repository).execute + + log_rollback_error(snippet) if result[:status] == :error + end + end + + def log_rollback_error(snippet) + Gitlab::AppLogger.error("Repository #{snippet.full_path} in path #{snippet.disk_path} could not be rolled back") + end + + def service_response_error(message, http_status) + ServiceResponse.error(message: message, http_status: http_status) + end + end +end diff --git a/app/services/snippets/destroy_service.rb b/app/services/snippets/destroy_service.rb index c1e87e74aa4..977626fcf17 100644 --- a/app/services/snippets/destroy_service.rb +++ b/app/services/snippets/destroy_service.rb @@ -4,12 +4,13 @@ module Snippets class DestroyService include Gitlab::Allowable - attr_reader :current_user, :project + attr_reader :current_user, :snippet + + DestroyError = Class.new(StandardError) def initialize(user, snippet) @current_user = user @snippet = snippet - @project = snippet&.project end def execute @@ -24,16 +25,29 @@ module Snippets ) end - if snippet.destroy - ServiceResponse.success(message: 'Snippet was deleted.') - else - service_response_error('Failed to remove snippet.', 400) - end + attempt_destroy! + + ServiceResponse.success(message: 'Snippet was deleted.') + rescue DestroyError + service_response_error('Failed to remove snippet repository.', 400) + rescue + attempt_rollback_repository + service_response_error('Failed to remove snippet.', 400) end private - attr_reader :snippet + def attempt_destroy! + result = Repositories::DestroyService.new(snippet.repository).execute + + raise DestroyError if result[:status] == :error + + snippet.destroy! + end + + def attempt_rollback_repository + Repositories::DestroyRollbackService.new(snippet.repository).execute + end def user_can_delete_snippet? can?(current_user, :admin_snippet, snippet) diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index ef79ee3d06e..587a8516394 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -56,10 +56,13 @@ module Users MigrateToGhostUserService.new(user).execute unless options[:hard_delete] + response = Snippets::BulkDestroyService.new(current_user, user.snippets).execute + raise DestroyError, response.message if response.error? + # Rails attempts to load all related records into memory before # destroying: https://github.com/rails/rails/issues/22510 # This ensures we delete records in batches. - user.destroy_dependent_associations_in_batches + user.destroy_dependent_associations_in_batches(exclude: [:snippets]) # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing user_data = user.destroy diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index e6b8e299e1c..b5a27f2f17d 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -4,7 +4,7 @@ #js-vue-sidebar-assignees{ data: { field: "#{issuable_type}", signed_in: signed_in } } .title.hide-collapsed = _('Assignee') - = icon('spinner spin') + .spinner.spinner-sm.align-bottom .selectbox.hide-collapsed - if assignees.none? |