summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue6
-rw-r--r--app/models/clusters/applications/elastic_stack.rb3
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/milestone.rb17
-rw-r--r--app/services/projects/destroy_service.rb19
-rw-r--r--app/services/repositories/base_service.rb8
-rw-r--r--app/services/repositories/destroy_service.rb4
-rw-r--r--app/services/snippets/bulk_destroy_service.rb74
-rw-r--r--app/services/snippets/destroy_service.rb30
-rw-r--r--app/services/users/destroy_service.rb5
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
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?