summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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
-rw-r--r--changelogs/unreleased/fj-39515-delete-snippet-repositories.yml5
-rw-r--r--changelogs/unreleased/issue_11391.yml5
-rw-r--r--changelogs/unreleased/rk4bir-master-patch-92247.yml5
-rw-r--r--doc/administration/incoming_email.md67
-rw-r--r--doc/administration/instance_limits.md18
-rw-r--r--doc/api/README.md12
-rw-r--r--doc/api/epic_links.md10
-rw-r--r--doc/api/feature_flags.md8
-rw-r--r--doc/api/issues_statistics.md2
-rw-r--r--doc/api/members.md2
-rw-r--r--doc/api/oauth2.md14
-rw-r--r--doc/api/packages.md10
-rw-r--r--doc/api/pipeline_schedules.md18
-rw-r--r--doc/api/pipelines.md28
-rw-r--r--doc/api/project_aliases.md18
-rw-r--r--doc/api/project_badges.md12
-rw-r--r--doc/api/project_clusters.md4
-rw-r--r--doc/api/project_level_variables.md20
-rw-r--r--doc/api/project_snippets.md14
-rw-r--r--doc/api/project_statistics.md2
-rw-r--r--doc/api/project_templates.md5
-rw-r--r--doc/api/projects.md76
-rw-r--r--doc/api/protected_branches.md12
-rw-r--r--doc/api/protected_environments.md2
-rw-r--r--doc/api/protected_tags.md10
-rw-r--r--doc/ci/environments.md6
-rw-r--r--doc/ci/img/environments_deployment_cluster_v12_8.pngbin0 -> 58639 bytes
-rw-r--r--doc/security/webhooks.md3
-rw-r--r--doc/topics/autodevops/index.md28
-rw-r--r--doc/user/gitlab_com/index.md7
-rw-r--r--doc/user/project/clusters/index.md22
-rw-r--r--doc/user/project/integrations/webhooks.md29
-rw-r--r--lib/gitlab/import_export/base/relation_factory.rb6
-rw-r--r--lib/gitlab/import_export/group/import_export.yml1
-rw-r--r--lib/gitlab/import_export/project/import_export.yml1
-rw-r--r--locale/gitlab.pot18
-rw-r--r--spec/factories/snippets.rb2
-rw-r--r--spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb2
-rw-r--r--spec/frontend/sidebar/assignee_title_spec.js116
-rw-r--r--spec/javascripts/sidebar/assignee_title_spec.js123
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/base/relation_factory_spec.rb9
-rw-r--r--spec/lib/gitlab/import_export/group/tree_saver_spec.rb2
-rw-r--r--spec/models/milestone_spec.rb22
-rw-r--r--spec/models/snippet_spec.rb7
-rw-r--r--spec/services/projects/destroy_service_spec.rb35
-rw-r--r--spec/services/snippets/bulk_destroy_service_spec.rb161
-rw-r--r--spec/services/snippets/create_service_spec.rb2
-rw-r--r--spec/services/snippets/destroy_service_spec.rb81
-rw-r--r--spec/services/snippets/update_service_spec.rb2
-rw-r--r--spec/services/users/destroy_service_spec.rb54
62 files changed, 924 insertions, 365 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?
diff --git a/changelogs/unreleased/fj-39515-delete-snippet-repositories.yml b/changelogs/unreleased/fj-39515-delete-snippet-repositories.yml
new file mode 100644
index 00000000000..a5ed472ddfa
--- /dev/null
+++ b/changelogs/unreleased/fj-39515-delete-snippet-repositories.yml
@@ -0,0 +1,5 @@
+---
+title: Add/update services to delete snippets repositories
+merge_request: 22672
+author:
+type: added
diff --git a/changelogs/unreleased/issue_11391.yml b/changelogs/unreleased/issue_11391.yml
new file mode 100644
index 00000000000..4b6e83728d6
--- /dev/null
+++ b/changelogs/unreleased/issue_11391.yml
@@ -0,0 +1,5 @@
+---
+title: Update moved service desk issues notifications
+merge_request: 25640
+author:
+type: added
diff --git a/changelogs/unreleased/rk4bir-master-patch-92247.yml b/changelogs/unreleased/rk4bir-master-patch-92247.yml
new file mode 100644
index 00000000000..d38ee386b6b
--- /dev/null
+++ b/changelogs/unreleased/rk4bir-master-patch-92247.yml
@@ -0,0 +1,5 @@
+---
+title: Migrated from .fa-spinner to .spinner in app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+merge_request: 24919
+author: rk4bir
+type: changed
diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md
index 6a0249d85d8..dcc590bea9c 100644
--- a/doc/administration/incoming_email.md
+++ b/doc/administration/incoming_email.md
@@ -283,10 +283,17 @@ incoming_email:
idle_timeout: 60
```
-#### MS Exchange
+#### Microsoft Exchange Server
-Example configuration for Microsoft Exchange mail server with IMAP enabled. Assumes the
-catch-all mailbox incoming@exchange.example.com.
+Example configurations for Microsoft Exchange Server with IMAP enabled. Since
+Exchange does not support sub-addressing, only two options exist:
+
+- Catch-all mailbox (recommended for Exchange-only)
+- Dedicated email address (supports Reply by Email only)
+
+##### Catch-all mailbox
+
+Assumes the catch-all mailbox `incoming@exchange.example.com`.
Example for Omnibus installs:
@@ -335,11 +342,53 @@ incoming_email:
port: 993
# Whether the IMAP server uses SSL
ssl: true
- # Whether the IMAP server uses StartTLS
- start_tls: false
+```
- # The mailbox where incoming mail will end up. Usually "inbox".
- mailbox: "inbox"
- # The IDLE command timeout.
- idle_timeout: 60
+##### Dedicated email address
+
+Assumes the dedicated email address `incoming@exchange.example.com`.
+
+Example for Omnibus installs:
+
+```ruby
+gitlab_rails['incoming_email_enabled'] = true
+
+# Exchange does not support sub-addressing, and we're not using a catch-all mailbox so %{key} is not used here
+gitlab_rails['incoming_email_address'] = "incoming@exchange.example.com"
+
+# Email account username
+# Typically this is the userPrincipalName (UPN)
+gitlab_rails['incoming_email_email'] = "incoming@ad-domain.example.com"
+# Email account password
+gitlab_rails['incoming_email_password'] = "[REDACTED]"
+
+# IMAP server host
+gitlab_rails['incoming_email_host'] = "exchange.example.com"
+# IMAP server port
+gitlab_rails['incoming_email_port'] = 993
+# Whether the IMAP server uses SSL
+gitlab_rails['incoming_email_ssl'] = true
+```
+
+Example for source installs:
+
+```yaml
+incoming_email:
+ enabled: true
+
+ # Exchange does not support sub-addressing, and we're not using a catch-all mailbox so %{key} is not used here
+ address: "incoming@exchange.example.com"
+
+ # Email account username
+ # Typically this is the userPrincipalName (UPN)
+ user: "incoming@ad-domain.example.com"
+ # Email account password
+ password: "[REDACTED]"
+
+ # IMAP server host
+ host: "exchange.example.com"
+ # IMAP server port
+ port: 993
+ # Whether the IMAP server uses SSL
+ ssl: true
```
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index f4f70995731..9c64453dadd 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -37,23 +37,9 @@ Activity history for projects and individuals' profiles was limited to one year
## Number of webhooks
-A maximum number of webhooks applies to each GitLab.com tier. Limits apply to project and group webhooks.
+On GitLab.com, the [maximum number of webhooks](../user/gitlab_com/index.md#maximum-number-of-webhooks) per project, and per group, is limited.
-### Project Webhooks
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20730) in GitLab 12.6.
-
-Check the [Maximum number of project webhooks (per tier)](../user/project/integrations/webhooks.md#maximum-number-of-project-webhooks-per-tier) section in the Webhooks page.
-
-### Group Webhooks
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25129) in GitLab 12.9.
-
-Check the [Maximum number of group webhooks (per tier)](../user/project/integrations/webhooks.md#maximum-number-of-group-webhooks-per-tier) section in the Webhooks page.
-
-### Setting the limit on a self-hosted installation
-
-To set this limit on a self-hosted installation, run the following in the
+To set this limit on a self-managed installation, run the following in the
[GitLab Rails console](https://docs.gitlab.com/omnibus/maintenance/#starting-a-rails-console-session):
```ruby
diff --git a/doc/api/README.md b/doc/api/README.md
index a261e1e7dc6..639d5067dd7 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -260,7 +260,7 @@ returned with status code `404`:
Example of a valid API call and a request using cURL with sudo request,
providing a username:
-```
+```plaintext
GET /projects?private_token=<your_access_token>&sudo=username
```
@@ -271,7 +271,7 @@ curl --header "Private-Token: <your_access_token>" --header "Sudo: username" "ht
Example of a valid API call and a request using cURL with sudo request,
providing an ID:
-```
+```plaintext
GET /projects?private_token=<your_access_token>&sudo=23
```
@@ -444,7 +444,7 @@ URL-encoded.
For example, `/` is represented by `%2F`:
-```
+```plaintext
GET /api/v4/projects/diaspora%2Fdiaspora
```
@@ -460,7 +460,7 @@ URL-encoded.
For example, `/` is represented by `%2F`:
-```
+```plaintext
GET /api/v4/projects/1/branches/my%2Fbranch/commits
```
@@ -604,13 +604,13 @@ to a [W3 recommendation](http://www.w3.org/Addressing/URL/4_URI_Recommentations.
causes a `+` to be interpreted as a space. For example, in an ISO 8601 date, you may want to pass
a time in Mountain Standard Time, such as:
-```
+```plaintext
2017-10-17T23:11:13.000+05:30
```
The correct encoding for the query parameter would be:
-```
+```plaintext
2017-10-17T23:11:13.000%2B05:30
```
diff --git a/doc/api/epic_links.md b/doc/api/epic_links.md
index 0e2fb2653c4..5df91e106eb 100644
--- a/doc/api/epic_links.md
+++ b/doc/api/epic_links.md
@@ -15,7 +15,7 @@ Epics are available only in the [Ultimate/Gold tier](https://about.gitlab.com/pr
Gets all child epics of an epic.
-```
+```plaintext
GET /groups/:id/epics/:epic_iid/epics
```
@@ -69,7 +69,7 @@ Example response:
Creates an association between two epics, designating one as the parent epic and the other as the child epic. A parent epic can have multiple child epics. If the new child epic already belonged to another epic, it is unassigned from that previous parent.
-```
+```plaintext
POST /groups/:id/epics/:epic_iid/epics
```
@@ -122,7 +122,7 @@ Example response:
Creates a a new epic and associates it with provided parent epic. The response is LinkedEpic object.
-```
+```plaintext
POST /groups/:id/epics/:epic_iid/epics
```
@@ -155,7 +155,7 @@ Example response:
## Re-order a child epic
-```
+```plaintext
PUT /groups/:id/epics/:epic_iid/epics/:child_epic_id
```
@@ -212,7 +212,7 @@ Example response:
Unassigns a child epic from a parent epic.
-```
+```plaintext
DELETE /groups/:id/epics/:epic_iid/epics/:child_epic_id
```
diff --git a/doc/api/feature_flags.md b/doc/api/feature_flags.md
index 384708be5df..f95eb31c84c 100644
--- a/doc/api/feature_flags.md
+++ b/doc/api/feature_flags.md
@@ -15,7 +15,7 @@ are [paginated](README.md#pagination).
Gets all feature flags of the requested project.
-```
+```plaintext
GET /projects/:id/feature_flags
```
@@ -145,7 +145,7 @@ Example response:
Creates a new feature flag.
-```
+```plaintext
POST /projects/:id/feature_flags
```
@@ -219,7 +219,7 @@ Example response:
Gets a single feature flag.
-```
+```plaintext
GET /projects/:id/feature_flags/:name
```
@@ -294,7 +294,7 @@ Example response:
Deletes a feature flag.
-```
+```plaintext
DELETE /projects/:id/feature_flags/:name
```
diff --git a/doc/api/issues_statistics.md b/doc/api/issues_statistics.md
index b24e385c3de..cb8379aba64 100644
--- a/doc/api/issues_statistics.md
+++ b/doc/api/issues_statistics.md
@@ -123,7 +123,7 @@ Example response:
Gets issues count statistics for given project.
-```
+```plaintext
GET /projects/:id/issues_statistics
GET /projects/:id/issues_statistics?labels=foo
GET /projects/:id/issues_statistics?labels=foo,bar
diff --git a/doc/api/members.md b/doc/api/members.md
index 47f2d694006..69c52a54a8d 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -4,7 +4,7 @@
The access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
-```
+```plaintext
10 => Guest access
20 => Reporter access
30 => Developer access
diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md
index 1991ad4bd14..50452b61c99 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -112,7 +112,7 @@ easily accessible, therefore secrets can leak easily.
To request the access token, you should redirect the user to the
`/oauth/authorize` endpoint using `token` response type:
-```
+```plaintext
https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=token&state=YOUR_UNIQUE_STATE_HASH&scope=REQUESTED_SCOPES
```
@@ -124,7 +124,7 @@ would request `read_user` and `profile` scopes). The redirect
will include a fragment with `access_token` as well as token details in GET
parameters, for example:
-```
+```plaintext
http://myapp.com/oauth/redirect#access_token=ABCDExyz123&state=YOUR_UNIQUE_STATE_HASH&token_type=bearer&expires_in=3600
```
@@ -182,7 +182,7 @@ curl --data "@auth.txt" --request POST https://gitlab.example.com/oauth/token
Then, you'll receive the access token back in the response:
-```
+```json
{
"access_token": "1f0af717251950dbd4d73154fdf0a474a5c5119adad999683f5b450c460726aa",
"token_type": "bearer",
@@ -192,7 +192,7 @@ Then, you'll receive the access token back in the response:
For testing, you can use the `oauth2` Ruby gem:
-```
+```ruby
client = OAuth2::Client.new('the_client_id', 'the_client_secret', :site => "http://example.com")
access_token = client.password.get_token('user@example.com', 'secret')
puts access_token.token
@@ -203,13 +203,13 @@ puts access_token.token
The `access token` allows you to make requests to the API on behalf of a user.
You can pass the token either as GET parameter:
-```
+```plaintext
GET https://gitlab.example.com/api/v4/user?access_token=OAUTH-TOKEN
```
or you can put the token to the Authorization header:
-```
+```shell
curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/user
```
@@ -222,7 +222,7 @@ You must supply the access token, either:
- As a parameter:
- ```
+ ```plaintext
GET https://gitlab.example.com/oauth/token/info?access_token=<OAUTH-TOKEN>
```
diff --git a/doc/api/packages.md b/doc/api/packages.md
index e04cb44538a..097f9dff84e 100644
--- a/doc/api/packages.md
+++ b/doc/api/packages.md
@@ -11,7 +11,7 @@ This is the API docs of [GitLab Packages](../administration/packages/index.md).
Get a list of project packages. All package types are included in results. When
accessed without authentication, only packages of public projects are returned.
-```
+```plaintext
GET /projects/:id/packages
```
@@ -56,7 +56,7 @@ By default, the `GET` request will return 20 results, since the API is [paginate
Get a list of project packages at the group level.
When accessed without authentication, only packages of public projects are returned.
-```
+```plaintext
GET /groups/:id/packages
```
@@ -135,7 +135,7 @@ The `_links` object contains the following properties:
Get a single project package.
-```
+```plaintext
GET /projects/:id/packages/:package_id
```
@@ -186,7 +186,7 @@ The `_links` object contains the following properties:
Get a list of package files of a single package.
-```
+```plaintext
GET /projects/:id/packages/:package_id/package_files
```
@@ -241,7 +241,7 @@ By default, the `GET` request will return 20 results, since the API is [paginate
Deletes a project package.
-```
+```plaintext
DELETE /projects/:id/packages/:package_id
```
diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md
index 3624921fde7..9ff641fc552 100644
--- a/doc/api/pipeline_schedules.md
+++ b/doc/api/pipeline_schedules.md
@@ -6,7 +6,7 @@ You can read more about [pipeline schedules](../user/project/pipelines/schedules
Get a list of the pipeline schedules of a project.
-```
+```plaintext
GET /projects/:id/pipeline_schedules
```
@@ -47,7 +47,7 @@ curl --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/
Get the pipeline schedule of a project.
-```
+```plaintext
GET /projects/:id/pipeline_schedules/:pipeline_schedule_id
```
@@ -99,7 +99,7 @@ curl --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/
Create a new pipeline schedule of a project.
-```
+```plaintext
POST /projects/:id/pipeline_schedules
```
@@ -143,7 +143,7 @@ curl --request POST --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form descri
Updates the pipeline schedule of a project. Once the update is done, it will be rescheduled automatically.
-```
+```plaintext
PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id
```
@@ -193,7 +193,7 @@ curl --request PUT --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form cron="0
Update the owner of the pipeline schedule of a project.
-```
+```plaintext
POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/take_ownership
```
@@ -238,7 +238,7 @@ curl --request POST --header "PRIVATE-TOKEN: hf2CvZXB9w8Uc5pZKpSB" "https://gitl
Delete the pipeline schedule of a project.
-```
+```plaintext
DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id
```
@@ -317,7 +317,7 @@ Example response:
Create a new variable of a pipeline schedule.
-```
+```plaintext
POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables
```
@@ -345,7 +345,7 @@ curl --request POST --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form "key=N
Updates the variable of a pipeline schedule.
-```
+```plaintext
PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key
```
@@ -373,7 +373,7 @@ curl --request PUT --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form "value=
Delete the variable of a pipeline schedule.
-```
+```plaintext
DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key
```
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index a3835045e5c..67d9348ba92 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -4,7 +4,7 @@
> [Introduced][ce-5837] in GitLab 8.11
-```
+```plaintext
GET /projects/:id/pipelines
```
@@ -23,7 +23,7 @@ GET /projects/:id/pipelines
| `order_by`| string | no | Order pipelines by `id`, `status`, `ref`, `updated_at` or `user_id` (default: `id`) |
| `sort` | string | no | Sort pipelines in `asc` or `desc` order (default: `desc`) |
-```
+```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipelines"
```
@@ -56,7 +56,7 @@ Example of response
> [Introduced][ce-5837] in GitLab 8.11
-```
+```plaintext
GET /projects/:id/pipelines/:pipeline_id
```
@@ -65,7 +65,7 @@ GET /projects/:id/pipelines/:pipeline_id
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_id` | integer | yes | The ID of a pipeline |
-```
+```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipelines/46"
```
@@ -101,7 +101,7 @@ Example of response
### Get variables of a pipeline
-```
+```plaintext
GET /projects/:id/pipelines/:pipeline_id/variables
```
@@ -110,7 +110,7 @@ GET /projects/:id/pipelines/:pipeline_id/variables
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_id` | integer | yes | The ID of a pipeline |
-```
+```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/variables"
```
@@ -134,7 +134,7 @@ Example of response
> [Introduced][ce-7209] in GitLab 8.14
-```
+```plaintext
POST /projects/:id/pipeline
```
@@ -144,7 +144,7 @@ POST /projects/:id/pipeline
| `ref` | string | yes | Reference to commit |
| `variables` | array | no | An array containing the variables available in the pipeline, matching the structure `[{ 'key' => 'UPLOAD_TO_S3', 'variable_type' => 'file', 'value' => 'true' }]` |
-```
+```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipeline?ref=master"
```
@@ -182,7 +182,7 @@ Example of response
> [Introduced][ce-5837] in GitLab 8.11
-```
+```plaintext
POST /projects/:id/pipelines/:pipeline_id/retry
```
@@ -191,7 +191,7 @@ POST /projects/:id/pipelines/:pipeline_id/retry
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_id` | integer | yes | The ID of a pipeline |
-```
+```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/retry"
```
@@ -229,7 +229,7 @@ Response:
> [Introduced][ce-5837] in GitLab 8.11
-```
+```plaintext
POST /projects/:id/pipelines/:pipeline_id/cancel
```
@@ -238,7 +238,7 @@ POST /projects/:id/pipelines/:pipeline_id/cancel
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_id` | integer | yes | The ID of a pipeline |
-```
+```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/cancel"
```
@@ -276,7 +276,7 @@ Response:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22988) in GitLab 11.6.
-```
+```plaintext
DELETE /projects/:id/pipelines/:pipeline_id
```
@@ -285,7 +285,7 @@ DELETE /projects/:id/pipelines/:pipeline_id
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_id` | integer | yes | The ID of a pipeline |
-```
+```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" --request "DELETE" "https://gitlab.example.com/api/v4/projects/1/pipelines/46"
```
diff --git a/doc/api/project_aliases.md b/doc/api/project_aliases.md
index da8d7600c7c..59c0ffee76d 100644
--- a/doc/api/project_aliases.md
+++ b/doc/api/project_aliases.md
@@ -8,11 +8,11 @@ All methods require administrator authorization.
Get a list of all project aliases:
-```
+```plaintext
GET /project_aliases
```
-```
+```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/project_aliases"
```
@@ -37,7 +37,7 @@ Example response:
Get details of a project alias:
-```
+```plaintext
GET /project_aliases/:name
```
@@ -45,7 +45,7 @@ GET /project_aliases/:name
|-----------|--------|----------|-----------------------|
| `name` | string | yes | The name of the alias |
-```
+```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/project_aliases/gitlab"
```
@@ -64,7 +64,7 @@ Example response:
Add a new alias for a project. Responds with a 201 when successful,
400 when there are validation errors (e.g. alias already exists):
-```
+```plaintext
POST /project_aliases
```
@@ -73,13 +73,13 @@ POST /project_aliases
| `project_id` | integer/string | yes | The ID or path of the project. |
| `name` | string | yes | The name of the alias. Must be unique. |
-```
+```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/project_aliases" --form "project_id=1" --form "name=gitlab"
```
or
-```
+```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/project_aliases" --form "project_id=gitlab-org/gitlab" --form "name=gitlab"
```
@@ -98,7 +98,7 @@ Example response:
Removes a project aliases. Responds with a 204 when project alias
exists, 404 when it doesn't:
-```
+```plaintext
DELETE /project_aliases/:name
```
@@ -106,6 +106,6 @@ DELETE /project_aliases/:name
|-----------|--------|----------|-----------------------|
| `name` | string | yes | The name of the alias |
-```
+```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/project_aliases/gitlab"
```
diff --git a/doc/api/project_badges.md b/doc/api/project_badges.md
index 77c5d6cf37e..2e335d80947 100644
--- a/doc/api/project_badges.md
+++ b/doc/api/project_badges.md
@@ -16,7 +16,7 @@ Badges support placeholders that will be replaced in real time in both the link
Gets a list of a project's badges and its group badges.
-```
+```plaintext
GET /projects/:id/badges
```
@@ -58,7 +58,7 @@ Example response:
Gets a badge of a project.
-```
+```plaintext
GET /projects/:id/badges/:badge_id
```
@@ -88,7 +88,7 @@ Example response:
Adds a badge to a project.
-```
+```plaintext
POST /projects/:id/badges
```
@@ -119,7 +119,7 @@ Example response:
Updates a badge of a project.
-```
+```plaintext
PUT /projects/:id/badges/:badge_id
```
@@ -151,7 +151,7 @@ Example response:
Removes a badge from a project. Only project's badges will be removed by using this endpoint.
-```
+```plaintext
DELETE /projects/:id/badges/:badge_id
```
@@ -168,7 +168,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitl
Returns how the `link_url` and `image_url` final URLs would be after resolving the placeholder interpolation.
-```
+```plaintext
GET /projects/:id/badges/render
```
diff --git a/doc/api/project_clusters.md b/doc/api/project_clusters.md
index 78d32acd0d2..448d5966135 100644
--- a/doc/api/project_clusters.md
+++ b/doc/api/project_clusters.md
@@ -9,7 +9,7 @@ User will need at least maintainer access to use these endpoints.
Returns a list of project clusters.
-```
+```plaintext
GET /projects/:id/clusters
```
@@ -368,7 +368,7 @@ Example response:
Deletes an existing project cluster.
-```
+```plaintext
DELETE /projects/:id/clusters/:cluster_id
```
diff --git a/doc/api/project_level_variables.md b/doc/api/project_level_variables.md
index d4bda992f7c..fbeba9d6c7d 100644
--- a/doc/api/project_level_variables.md
+++ b/doc/api/project_level_variables.md
@@ -4,7 +4,7 @@
Get list of a project's variables.
-```
+```plaintext
GET /projects/:id/variables
```
@@ -12,7 +12,7 @@ GET /projects/:id/variables
|-----------|---------|----------|---------------------|
| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-```
+```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/variables"
```
@@ -35,7 +35,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
Get the details of a project's specific variable.
-```
+```plaintext
GET /projects/:id/variables/:key
```
@@ -44,7 +44,7 @@ GET /projects/:id/variables/:key
| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `key` | string | yes | The `key` of a variable |
-```
+```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/variables/TEST_VARIABLE_1"
```
@@ -62,7 +62,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
Create a new variable.
-```
+```plaintext
POST /projects/:id/variables
```
@@ -76,7 +76,7 @@ POST /projects/:id/variables
| `masked` | boolean | no | Whether the variable is masked |
| `environment_scope` | string | no | The `environment_scope` of the variable |
-```
+```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
```
@@ -95,7 +95,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
Update a project's variable.
-```
+```plaintext
PUT /projects/:id/variables/:key
```
@@ -109,7 +109,7 @@ PUT /projects/:id/variables/:key
| `masked` | boolean | no | Whether the variable is masked |
| `environment_scope` | string | no | The `environment_scope` of the variable |
-```
+```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
```
@@ -128,7 +128,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
Remove a project's variable.
-```
+```plaintext
DELETE /projects/:id/variables/:key
```
@@ -137,6 +137,6 @@ DELETE /projects/:id/variables/:key
| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `key` | string | yes | The `key` of a variable |
-```
+```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/variables/VARIABLE_1"
```
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index b734273b5f6..39df2925a1e 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -23,7 +23,7 @@ visibility setting keep this setting. You can read more about the change in the
Get a list of project snippets.
-```
+```plaintext
GET /projects/:id/snippets
```
@@ -35,7 +35,7 @@ Parameters:
Get a single project snippet.
-```
+```plaintext
GET /projects/:id/snippets/:snippet_id
```
@@ -68,7 +68,7 @@ Parameters:
Creates a new project snippet. The user must have permission to create new snippets.
-```
+```plaintext
POST /projects/:id/snippets
```
@@ -106,7 +106,7 @@ curl --request POST https://gitlab.com/api/v4/projects/:id/snippets \
Updates an existing project snippet. The user must have permission to change an existing snippet.
-```
+```plaintext
PUT /projects/:id/snippets/:snippet_id
```
@@ -145,7 +145,7 @@ curl --request PUT https://gitlab.com/api/v4/projects/:id/snippets/:snippet_id \
Deletes an existing project snippet. This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found.
-```
+```plaintext
DELETE /projects/:id/snippets/:snippet_id
```
@@ -165,7 +165,7 @@ curl --request DELETE https://gitlab.com/api/v4/projects/:id/snippets/:snippet_i
Returns the raw project snippet as plain text.
-```
+```plaintext
GET /projects/:id/snippets/:snippet_id/raw
```
@@ -187,7 +187,7 @@ curl https://gitlab.com/api/v4/projects/:id/snippets/:snippet_id/raw \
Available only for admins.
-```
+```plaintext
GET /projects/:id/snippets/:snippet_id/user_agent_detail
```
diff --git a/doc/api/project_statistics.md b/doc/api/project_statistics.md
index 2732fa47fa0..d96d3de6a73 100644
--- a/doc/api/project_statistics.md
+++ b/doc/api/project_statistics.md
@@ -8,7 +8,7 @@ Retrieving the statistics requires write access to the repository.
Currently only HTTP fetches statistics are returned.
Fetches statistics includes both clones and pulls count and are HTTP only, SSH fetches are not included.
-```
+```plaintext
GET /projects/:id/statistics
```
diff --git a/doc/api/project_templates.md b/doc/api/project_templates.md
index d6ad77de429..4062df24525 100644
--- a/doc/api/project_templates.md
+++ b/doc/api/project_templates.md
@@ -21,7 +21,7 @@ in GitLab 11.5
## Get all templates of a particular type
-```
+```plaintext
GET /projects/:id/templates/:type
```
@@ -87,7 +87,7 @@ Example response (licenses):
## Get one template of a particular type
-```
+```plaintext
GET /projects/:id/templates/:type/:key
```
@@ -106,7 +106,6 @@ Example response (Dockerfile):
"name": "Binary",
"content": "# This file is a template, and might need editing before it works on your project.\n# This Dockerfile installs a compiled binary into a bare system.\n# You must either commit your compiled binary into source control (not recommended)\n# or build the binary first as part of a CI/CD pipeline.\n\nFROM buildpack-deps:jessie\n\nWORKDIR /usr/local/bin\n\n# Change `app` to whatever your binary is called\nAdd app .\nCMD [\"./app\"]\n"
}
-
```
Example response (license):
diff --git a/doc/api/projects.md b/doc/api/projects.md
index a0243be1907..905eb01b0ad 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -35,7 +35,7 @@ There are currently three options for `merge_method` to choose from:
Get a list of all visible projects across GitLab for the authenticated user.
When accessed without authentication, only public projects with "simple" fields are returned.
-```
+```plaintext
GET /projects
```
@@ -298,7 +298,7 @@ the `approvals_before_merge` parameter:
You can filter by [custom attributes](custom_attributes.md) with:
-```
+```plaintext
GET /projects?custom_attributes[key]=value&custom_attributes[other_key]=other_value
```
@@ -315,7 +315,7 @@ Note that keyset pagination only supports `order_by=id`. Other sorting options a
Get a list of visible projects owned by the given user. When accessed without authentication, only public projects are returned.
-```
+```plaintext
GET /users/:user_id/projects
```
@@ -530,7 +530,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo
Get a list of visible projects owned by the given user. When accessed without authentication, only public projects are returned.
-```
+```plaintext
GET /users/:user_id/starred_projects
```
@@ -740,7 +740,7 @@ Example response:
Get a specific project. This endpoint can be accessed without authentication if
the project is publicly accessible.
-```
+```plaintext
GET /projects/:id
```
@@ -955,7 +955,7 @@ If the project is a fork, and you provide a valid token to authenticate, the
Get the users list of a project.
-```
+```plaintext
GET /projects/:id/users
```
@@ -993,7 +993,7 @@ Please refer to the [Events API documentation](events.md#list-a-projects-visible
Creates a new project owned by the authenticated user.
-```
+```plaintext
POST /projects
```
@@ -1061,7 +1061,7 @@ where `password` is a public access key with the `api` scope enabled.
Creates a new project owned by the specified user. Available only for admins.
-```
+```plaintext
POST /projects/user/:user_id
```
@@ -1128,7 +1128,7 @@ where `password` is a public access key with the `api` scope enabled.
Updates an existing project.
-```
+```plaintext
PUT /projects/:id
```
@@ -1200,7 +1200,7 @@ The forking operation for a project is asynchronous and is completed in a
background job. The request will return immediately. To determine whether the
fork of the project has completed, query the `import_status` for the new project.
-```
+```plaintext
POST /projects/:id/fork
```
@@ -1217,7 +1217,7 @@ POST /projects/:id/fork
List the projects accessible to the calling user that have an established, forked relationship with the specified project
-```
+```plaintext
GET /projects/:id/forks
```
@@ -1315,7 +1315,7 @@ Example responses:
Stars a given project. Returns status code `304` if the project is already starred.
-```
+```plaintext
POST /projects/:id/star
```
@@ -1405,7 +1405,7 @@ Example response:
Unstars a given project. Returns status code `304` if the project is not starred.
-```
+```plaintext
POST /projects/:id/unstar
```
@@ -1495,7 +1495,7 @@ Example response:
List the users who starred the specified project.
-```
+```plaintext
GET /projects/:id/starrers
```
@@ -1540,7 +1540,7 @@ Example responses:
Get languages used in a project with percentage value.
-```
+```plaintext
GET /projects/:id/languages
```
@@ -1564,7 +1564,7 @@ Example response:
Archives the project if the user is either admin or the project owner of this project. This action is
idempotent, thus archiving an already archived project will not change the project.
-```
+```plaintext
POST /projects/:id/archive
```
@@ -1673,7 +1673,7 @@ Example response:
Unarchives the project if the user is either admin or the project owner of this project. This action is
idempotent, thus unarchiving a non-archived project will not change the project.
-```
+```plaintext
POST /projects/:id/unarchive
```
@@ -1786,7 +1786,7 @@ This endpoint either:
deletion happens after number of days specified in
[instance settings](../user/admin_area/settings/visibility_and_access_controls.md#default-deletion-adjourned-period-premium-only).
-```
+```plaintext
DELETE /projects/:id
```
@@ -1800,7 +1800,7 @@ DELETE /projects/:id
Restores project marked for deletion.
-```
+```plaintext
POST /projects/:id/restore
```
@@ -1812,7 +1812,7 @@ POST /projects/:id/restore
Uploads a file to the specified project to be used in an issue or merge request description, or a comment.
-```
+```plaintext
POST /projects/:id/uploads
```
@@ -1848,7 +1848,7 @@ In Markdown contexts, the link is automatically expanded when the format in
Allow to share project with group.
-```
+```plaintext
POST /projects/:id/share
```
@@ -1863,7 +1863,7 @@ POST /projects/:id/share
Unshare the project from the group. Returns `204` and no content on success.
-```
+```plaintext
DELETE /projects/:id/share/:group_id
```
@@ -1885,7 +1885,7 @@ These are different for [System Hooks](system_hooks.md) that are system wide.
Get a list of project hooks.
-```
+```plaintext
GET /projects/:id/hooks
```
@@ -1897,7 +1897,7 @@ GET /projects/:id/hooks
Get a specific hook for a project.
-```
+```plaintext
GET /projects/:id/hooks/:hook_id
```
@@ -1930,7 +1930,7 @@ GET /projects/:id/hooks/:hook_id
Adds a hook to a specified project.
-```
+```plaintext
POST /projects/:id/hooks
```
@@ -1955,7 +1955,7 @@ POST /projects/:id/hooks
Edits a hook for a specified project.
-```
+```plaintext
PUT /projects/:id/hooks/:hook_id
```
@@ -1982,7 +1982,7 @@ PUT /projects/:id/hooks/:hook_id
Removes a hook from a project. This is an idempotent method and can be called multiple times.
Either the hook is available or not.
-```
+```plaintext
DELETE /projects/:id/hooks/:hook_id
```
@@ -2000,7 +2000,7 @@ Allows modification of the forked relationship between existing projects. Availa
### Create a forked from/to relation between existing projects
-```
+```plaintext
POST /projects/:id/fork/:forked_from_id
```
@@ -2011,7 +2011,7 @@ POST /projects/:id/fork/:forked_from_id
### Delete an existing forked from relationship
-```
+```plaintext
DELETE /projects/:id/fork
```
@@ -2025,7 +2025,7 @@ Search for projects by name which are accessible to the authenticated user. This
endpoint can be accessed without authentication if the project is publicly
accessible.
-```
+```plaintext
GET /projects
```
@@ -2043,7 +2043,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap
> Introduced in GitLab 9.0.
-```
+```plaintext
POST /projects/:id/housekeeping
```
@@ -2057,7 +2057,7 @@ POST /projects/:id/housekeeping
Get the push rules of a project.
-```
+```plaintext
GET /projects/:id/push_rule
```
@@ -2101,7 +2101,7 @@ the `commit_committer_check` and `reject_unsigned_commits` parameters:
Adds a push rule to a specified project.
-```
+```plaintext
POST /projects/:id/push_rule
```
@@ -2124,7 +2124,7 @@ POST /projects/:id/push_rule
Edits a push rule for a specified project.
-```
+```plaintext
PUT /projects/:id/push_rule
```
@@ -2150,7 +2150,7 @@ PUT /projects/:id/push_rule
Removes a push rule from a project. This is an idempotent method and can be called multiple times.
Either the push rule is available or not.
-```
+```plaintext
DELETE /projects/:id/push_rule
```
@@ -2162,7 +2162,7 @@ DELETE /projects/:id/push_rule
> Introduced in GitLab 11.1.
-```
+```plaintext
PUT /projects/:id/transfer
```
@@ -2186,7 +2186,7 @@ Read more in the [Project members](members.md) documentation.
> Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
-```
+```plaintext
POST /projects/:id/mirror/pull
```
@@ -2219,7 +2219,7 @@ format.
If a repository is corrupted to the point where `git clone` does not work, the
snapshot may allow some of the data to be retrieved.
-```
+```plaintext
GET /projects/:id/snapshot
```
diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md
index e59d7130356..de862109055 100644
--- a/doc/api/protected_branches.md
+++ b/doc/api/protected_branches.md
@@ -6,7 +6,7 @@
The access levels are defined in the `ProtectedRefAccess.allowed_access_levels` method. Currently, these levels are recognized:
-```
+```plaintext
0 => No access
30 => Developer access
40 => Maintainer access
@@ -17,7 +17,7 @@ The access levels are defined in the `ProtectedRefAccess.allowed_access_levels`
Gets a list of protected branches from a project.
-```
+```plaintext
GET /projects/:id/protected_branches
```
@@ -91,7 +91,7 @@ Example response:
Gets a single protected branch or wildcard protected branch.
-```
+```plaintext
GET /projects/:id/protected_branches/:name
```
@@ -160,7 +160,7 @@ Example response:
Protects a single repository branch or several project repository
branches using a wildcard protected branch.
-```
+```plaintext
POST /projects/:id/protected_branches
```
@@ -292,7 +292,7 @@ Example response:
Unprotects the given protected branch or wildcard protected branch.
-```
+```plaintext
DELETE /projects/:id/protected_branches/:name
```
@@ -309,7 +309,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" 'https://git
Update the "code owner approval required" option for the given protected branch protected branch.
-```
+```plaintext
PATCH /projects/:id/protected_branches/:name
```
diff --git a/doc/api/protected_environments.md b/doc/api/protected_environments.md
index 852a5ae6e71..dea1382af29 100644
--- a/doc/api/protected_environments.md
+++ b/doc/api/protected_environments.md
@@ -7,7 +7,7 @@
The access levels are defined in the `ProtectedEnvironment::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method.
Currently, these levels are recognized:
-```
+```plaintext
30 => Developer access
40 => Maintainer access
60 => Admin access
diff --git a/doc/api/protected_tags.md b/doc/api/protected_tags.md
index a5490094a44..1d844a2c5c4 100644
--- a/doc/api/protected_tags.md
+++ b/doc/api/protected_tags.md
@@ -6,7 +6,7 @@
Currently, these levels are recognized:
-```
+```plaintext
0 => No access
30 => Developer access
40 => Maintainer access
@@ -17,7 +17,7 @@ Currently, these levels are recognized:
Gets a list of protected tags from a project.
This function takes pagination parameters `page` and `per_page` to restrict the list of protected tags.
-```
+```plaintext
GET /projects/:id/protected_tags
```
@@ -51,7 +51,7 @@ Example response:
Gets a single protected tag or wildcard protected tag.
The pagination parameters `page` and `per_page` can be used to restrict the list of protected tags.
-```
+```plaintext
GET /projects/:id/protected_tags/:name
```
@@ -83,7 +83,7 @@ Example response:
Protects a single repository tag or several project repository
tags using a wildcard protected tag.
-```
+```plaintext
POST /projects/:id/protected_tags
```
@@ -115,7 +115,7 @@ Example response:
Unprotects the given protected tag or wildcard protected tag.
-```
+```plaintext
DELETE /projects/:id/protected_tags/:name
```
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 166ed9b4571..ff4096600e0 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -340,6 +340,12 @@ deploy:
- master
```
+When deploying to a Kubernetes cluster using GitLab's Kubernetes integration,
+information about the cluster and namespace will be displayed above the job
+trace on the deployment job page:
+
+![Deployment cluster information](img/environments_deployment_cluster_v12_8.png)
+
NOTE: **Note:**
Kubernetes configuration is not supported for Kubernetes clusters
that are [managed by GitLab](../user/project/clusters/index.md#gitlab-managed-clusters).
diff --git a/doc/ci/img/environments_deployment_cluster_v12_8.png b/doc/ci/img/environments_deployment_cluster_v12_8.png
new file mode 100644
index 00000000000..dfda1deb649
--- /dev/null
+++ b/doc/ci/img/environments_deployment_cluster_v12_8.png
Binary files differ
diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md
index 010f5aa2d43..7313cffdd13 100644
--- a/doc/security/webhooks.md
+++ b/doc/security/webhooks.md
@@ -4,6 +4,9 @@ type: concepts, reference, howto
# Webhooks and insecure internal web services
+NOTE: **Note:**
+On GitLab.com the [maximum number of webhooks](../user/gitlab_com/index.md#maximum-number-of-webhooks) per project is limited.
+
If you have non-GitLab web services running on your GitLab server or within its
local network, these may be vulnerable to exploitation via Webhooks.
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index a6e7255df3d..af33936297c 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -181,7 +181,7 @@ To make full use of Auto DevOps, you will need:
If you have configured GitLab's Kubernetes integration, you can deploy it to
your cluster by installing the
[GitLab-managed app for cert-manager](../../user/clusters/applications.md#cert-manager).
-
+
If you do not have Kubernetes or Prometheus installed, then Auto Review Apps,
Auto Deploy, and Auto Monitoring will be silently skipped.
@@ -1030,6 +1030,32 @@ It is also possible to copy and paste the contents of the [Auto DevOps
template] into your project and edit this as needed. You may prefer to do it
that way if you want to specifically remove any part of it.
+### Customizing the Kubernetes namespace
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27630) in GitLab 12.6.
+
+For **non**-GitLab-managed clusters, the namespace can be customized using
+`.gitlab-ci.yml` by specifying
+[`environment:kubernetes:namespace`](../../ci/environments.md#configuring-kubernetes-deployments).
+For example, the following configuration overrides the namespace used for
+`production` deployments:
+
+```yaml
+include:
+ - template: Auto-DevOps.gitlab-ci.yml
+
+production:
+ environment:
+ kubernetes:
+ namespace: production
+```
+
+When deploying to a custom namespace with Auto DevOps, the service account
+provided with the cluster needs at least the `edit` role within the namespace.
+
+- If the service account can create namespaces, then the namespace can be created on-demand.
+- Otherwise, the namespace must exist prior to deployment.
+
### Using components of Auto DevOps
If you only require a subset of the features offered by Auto DevOps, you can include
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 143da6b7fd6..16b5f94162b 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -94,6 +94,13 @@ IP based firewall can be configured by looking up all
[Static endpoints](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/97) are being considered.
+## Maximum number of webhooks
+
+A limit of:
+
+- 100 webhooks applies to projects.
+- 50 webhooks applies to groups. **(BRONZE ONLY)**
+
## Shared Runners
GitLab offers Linux and Windows shared runners hosted on GitLab.com for executing your pipelines.
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 819e5a26c22..e221d81c280 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -281,22 +281,28 @@ GitLab CI/CD build environment.
| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. This config also embeds the same token defined in `KUBE_TOKEN` so you likely will only need this variable. This variable name is also automatically picked up by `kubectl` so you won't actually need to reference it explicitly if using `kubectl`. |
| `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, this variable can be used to set a domain per cluster. See [cluster domains](#base-domain) for more information. |
-NOTE: **NOTE:**
+NOTE: **Note:**
Prior to GitLab 11.5, `KUBE_TOKEN` was the Kubernetes token of the main
service account of the cluster integration.
NOTE: **Note:**
If your cluster was created before GitLab 12.2, default `KUBE_NAMESPACE` will be set to `<project_name>-<project_id>`.
-When deploying a custom namespace:
+### Custom namespace
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27630) in GitLab 12.6.
+
+The Kubernetes integration defaults to project-environment-specific namespaces
+of the form `<project_name>-<project_id>-<environment>` (see [Deployment
+variables](#deployment-variables)).
-- The custom namespace must exist in your cluster.
-- The project's deployment service account must have permission to deploy to the namespace.
-- `KUBECONFIG` must be updated to use the custom namespace instead of the GitLab-provided default (this is [not automatic](https://gitlab.com/gitlab-org/gitlab/issues/31519)).
-- If deploying with Auto DevOps, you must *also* override `KUBE_NAMESPACE` with the custom namespace.
+For **non**-GitLab-managed clusters, the namespace can be customized using
+[`environment:kubernetes:namespace`](../../../ci/environments.md#configuring-kubernetes-deployments)
+in `.gitlab-ci.yml`.
-CAUTION: **Caution:**
-GitLab does not save custom namespaces in the database. So while deployments work with custom namespaces, GitLab's integration for already-deployed environments will not pick up the customized values. For example, [Deploy Boards](../deploy_boards.md) will not work as intended for those deployments. For more information, see the [related issue](https://gitlab.com/gitlab-org/gitlab/issues/27630).
+NOTE: **Note:** When using a [GitLab-managed cluster](#gitlab-managed-clusters), the
+namespaces are created automatically prior to deployment and [can not be
+customized](https://gitlab.com/gitlab-org/gitlab/issues/38054).
### Troubleshooting
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 0896faa5e9d..2deee360bde 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -47,33 +47,8 @@ and **per project and per group** for **GitLab Enterprise Edition**.
Navigate to the webhooks page by going to your project's
**Settings ➔ Webhooks**.
-## Maximum number of project webhooks (per tier)
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20730) in GitLab 12.6.
-
-A maximum number of project webhooks applies to each [GitLab.com
-tier](https://about.gitlab.com/pricing/), as shown in the following table:
-
-| Tier | Number of webhooks per project |
-|----------|--------------------------------|
-| Free | 100 |
-| Bronze | 100 |
-| Silver | 100 |
-| Gold | 100 |
-
-## Maximum number of group webhooks (per tier)
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25129) in GitLab 12.9.
-
-A maximum number of group webhooks applies to each [GitLab.com
-tier](https://about.gitlab.com/pricing/), as shown in the following table:
-
-| Tier | Number of webhooks per group |
-|----------|--------------------------------|
-| Free | feature not available |
-| Bronze | 50 |
-| Silver | 50 |
-| Gold | 50 |
+NOTE: **Note:**
+On GitLab.com, the [maximum number of webhooks](../../../user/gitlab_com/index.md#maximum-number-of-webhooks) per project, and per group, is limited.
## Use-cases
diff --git a/lib/gitlab/import_export/base/relation_factory.rb b/lib/gitlab/import_export/base/relation_factory.rb
index 688627d1f2f..05b69362976 100644
--- a/lib/gitlab/import_export/base/relation_factory.rb
+++ b/lib/gitlab/import_export/base/relation_factory.rb
@@ -67,7 +67,7 @@ module Gitlab
# the relation_hash, updating references with new object IDs, mapping users using
# the "members_mapper" object, also updating notes if required.
def create
- return if invalid_relation?
+ return if invalid_relation? || predefined_relation?
setup_base_models
setup_models
@@ -89,6 +89,10 @@ module Gitlab
false
end
+ def predefined_relation?
+ relation_class.try(:predefined_id?, @relation_hash['id'])
+ end
+
def setup_models
raise NotImplementedError
end
diff --git a/lib/gitlab/import_export/group/import_export.yml b/lib/gitlab/import_export/group/import_export.yml
index d4e0ff12373..2721198860c 100644
--- a/lib/gitlab/import_export/group/import_export.yml
+++ b/lib/gitlab/import_export/group/import_export.yml
@@ -70,6 +70,7 @@ ee:
- :push_event_payload
- boards:
- :board_assignee
+ - :milestone
- labels:
- :priorities
- lists:
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index 4fa909ac94b..fd83a275e0d 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -188,6 +188,7 @@ excluded_attributes:
issues:
- :milestone_id
- :moved_to_id
+ - :sent_notifications
- :state_id
- :duplicated_to_id
- :promoted_to_epic_id
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ad20e87eb68..6d20a0e9ba8 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6260,6 +6260,9 @@ msgstr ""
msgid "DeleteProject|Failed to remove project repository. Please try again or contact administrator."
msgstr ""
+msgid "DeleteProject|Failed to remove project snippets. Please try again or contact administrator."
+msgstr ""
+
msgid "DeleteProject|Failed to remove some tags in project container registry. Please try again or contact administrator."
msgstr ""
@@ -20790,6 +20793,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to fetch unscanned projects"
+msgstr ""
+
msgid "Unable to fetch vulnerable projects"
msgstr ""
@@ -20904,6 +20910,18 @@ msgstr ""
msgid "Unresolve thread"
msgstr ""
+msgid "UnscannedProjects|15 or more days"
+msgstr ""
+
+msgid "UnscannedProjects|30 or more days"
+msgstr ""
+
+msgid "UnscannedProjects|5 or more days"
+msgstr ""
+
+msgid "UnscannedProjects|60 or more days"
+msgstr ""
+
msgid "UnscannedProjects|Default branch scanning by project"
msgstr ""
diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb
index 6fcb0319748..2cdb8019696 100644
--- a/spec/factories/snippets.rb
+++ b/spec/factories/snippets.rb
@@ -27,6 +27,8 @@ FactoryBot.define do
TestEnv.copy_repo(snippet,
bare_repo: TestEnv.factory_repo_path_bare,
refs: TestEnv::BRANCH_SHA)
+
+ snippet.track_snippet_repository
end
end
diff --git a/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb b/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb
index 26984a1fb5e..a38bc4f702b 100644
--- a/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb
+++ b/spec/features/merge_request/user_views_merge_request_from_deleted_fork_spec.rb
@@ -20,7 +20,7 @@ describe 'User views merged merge request from deleted fork' do
fork_owner = source_project.namespace.owners.first
# Place the source_project in the weird in between state
source_project.update_attribute(:pending_delete, true)
- Projects::DestroyService.new(source_project, fork_owner, {}).__send__(:trash_repositories!)
+ Projects::DestroyService.new(source_project, fork_owner, {}).__send__(:trash_project_repositories!)
end
it 'correctly shows the merge request' do
diff --git a/spec/frontend/sidebar/assignee_title_spec.js b/spec/frontend/sidebar/assignee_title_spec.js
new file mode 100644
index 00000000000..92fabaa664e
--- /dev/null
+++ b/spec/frontend/sidebar/assignee_title_spec.js
@@ -0,0 +1,116 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
+import Component from '~/sidebar/components/assignees/assignee_title.vue';
+
+describe('AssigneeTitle component', () => {
+ let wrapper;
+
+ const createComponent = props => {
+ return shallowMount(Component, {
+ propsData: {
+ numberOfAssignees: 0,
+ editable: false,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('assignee title', () => {
+ it('renders assignee', () => {
+ wrapper = createComponent({
+ numberOfAssignees: 1,
+ editable: false,
+ });
+
+ expect(wrapper.vm.$el.innerText.trim()).toEqual('Assignee');
+ });
+
+ it('renders 2 assignees', () => {
+ wrapper = createComponent({
+ numberOfAssignees: 2,
+ editable: false,
+ });
+
+ expect(wrapper.vm.$el.innerText.trim()).toEqual('2 Assignees');
+ });
+ });
+
+ describe('gutter toggle', () => {
+ it('does not show toggle by default', () => {
+ wrapper = createComponent({
+ numberOfAssignees: 2,
+ editable: false,
+ });
+
+ expect(wrapper.vm.$el.querySelector('.gutter-toggle')).toBeNull();
+ });
+
+ it('shows toggle when showToggle is true', () => {
+ wrapper = createComponent({
+ numberOfAssignees: 2,
+ editable: false,
+ showToggle: true,
+ });
+
+ expect(wrapper.vm.$el.querySelector('.gutter-toggle')).toEqual(expect.any(Object));
+ });
+ });
+
+ it('does not render spinner by default', () => {
+ wrapper = createComponent({
+ numberOfAssignees: 0,
+ editable: false,
+ });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
+ });
+
+ it('renders spinner when loading', () => {
+ wrapper = createComponent({
+ loading: true,
+ numberOfAssignees: 0,
+ editable: false,
+ });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBeTruthy();
+ });
+
+ it('does not render edit link when not editable', () => {
+ wrapper = createComponent({
+ numberOfAssignees: 0,
+ editable: false,
+ });
+
+ expect(wrapper.vm.$el.querySelector('.edit-link')).toBeNull();
+ });
+
+ it('renders edit link when editable', () => {
+ wrapper = createComponent({
+ numberOfAssignees: 0,
+ editable: true,
+ });
+
+ expect(wrapper.vm.$el.querySelector('.edit-link')).not.toBeNull();
+ });
+
+ it('tracks the event when edit is clicked', () => {
+ wrapper = createComponent({
+ numberOfAssignees: 0,
+ editable: true,
+ });
+
+ const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
+ triggerEvent('.js-sidebar-dropdown-toggle');
+
+ expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
+ label: 'right_sidebar',
+ property: 'assignee',
+ });
+ });
+});
diff --git a/spec/javascripts/sidebar/assignee_title_spec.js b/spec/javascripts/sidebar/assignee_title_spec.js
deleted file mode 100644
index 0496e280a21..00000000000
--- a/spec/javascripts/sidebar/assignee_title_spec.js
+++ /dev/null
@@ -1,123 +0,0 @@
-import Vue from 'vue';
-import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
-import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue';
-
-describe('AssigneeTitle component', () => {
- let component;
- let AssigneeTitleComponent;
-
- beforeEach(() => {
- AssigneeTitleComponent = Vue.extend(AssigneeTitle);
- });
-
- describe('assignee title', () => {
- it('renders assignee', () => {
- component = new AssigneeTitleComponent({
- propsData: {
- numberOfAssignees: 1,
- editable: false,
- },
- }).$mount();
-
- expect(component.$el.innerText.trim()).toEqual('Assignee');
- });
-
- it('renders 2 assignees', () => {
- component = new AssigneeTitleComponent({
- propsData: {
- numberOfAssignees: 2,
- editable: false,
- },
- }).$mount();
-
- expect(component.$el.innerText.trim()).toEqual('2 Assignees');
- });
- });
-
- describe('gutter toggle', () => {
- it('does not show toggle by default', () => {
- component = new AssigneeTitleComponent({
- propsData: {
- numberOfAssignees: 2,
- editable: false,
- },
- }).$mount();
-
- expect(component.$el.querySelector('.gutter-toggle')).toBeNull();
- });
-
- it('shows toggle when showToggle is true', () => {
- component = new AssigneeTitleComponent({
- propsData: {
- numberOfAssignees: 2,
- editable: false,
- showToggle: true,
- },
- }).$mount();
-
- expect(component.$el.querySelector('.gutter-toggle')).toEqual(jasmine.any(Object));
- });
- });
-
- it('does not render spinner by default', () => {
- component = new AssigneeTitleComponent({
- propsData: {
- numberOfAssignees: 0,
- editable: false,
- },
- }).$mount();
-
- expect(component.$el.querySelector('.fa')).toBeNull();
- });
-
- it('renders spinner when loading', () => {
- component = new AssigneeTitleComponent({
- propsData: {
- loading: true,
- numberOfAssignees: 0,
- editable: false,
- },
- }).$mount();
-
- expect(component.$el.querySelector('.fa')).not.toBeNull();
- });
-
- it('does not render edit link when not editable', () => {
- component = new AssigneeTitleComponent({
- propsData: {
- numberOfAssignees: 0,
- editable: false,
- },
- }).$mount();
-
- expect(component.$el.querySelector('.edit-link')).toBeNull();
- });
-
- it('renders edit link when editable', () => {
- component = new AssigneeTitleComponent({
- propsData: {
- numberOfAssignees: 0,
- editable: true,
- },
- }).$mount();
-
- expect(component.$el.querySelector('.edit-link')).not.toBeNull();
- });
-
- it('tracks the event when edit is clicked', () => {
- component = new AssigneeTitleComponent({
- propsData: {
- numberOfAssignees: 0,
- editable: true,
- },
- }).$mount();
-
- const spy = mockTracking('_category_', component.$el, spyOn);
- triggerEvent('.js-sidebar-dropdown-toggle');
-
- expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
- label: 'right_sidebar',
- property: 'assignee',
- });
- });
-});
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index d97d76cf35e..f6a3ade7f18 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -10,6 +10,7 @@ issues:
- resource_label_events
- resource_weight_events
- resource_milestone_events
+- sent_notifications
- sentry_issue
- label_links
- labels
diff --git a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
index 1011de83c95..50d93763ad6 100644
--- a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
@@ -33,6 +33,15 @@ describe Gitlab::ImportExport::Base::RelationFactory do
end
end
+ context 'when the relation is predefined' do
+ let(:relation_sym) { :milestone }
+ let(:relation_hash) { { 'name' => '#upcoming', 'title' => 'Upcoming', 'id' => -2 } }
+
+ it 'returns without creating a new relation' do
+ expect(subject).to be_nil
+ end
+ end
+
context 'when #setup_models is not implemented' do
it 'raises NotImplementedError' do
expect { subject }.to raise_error(NotImplementedError)
diff --git a/spec/lib/gitlab/import_export/group/tree_saver_spec.rb b/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
index 845eb8e308b..a7440ac24ca 100644
--- a/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
@@ -189,7 +189,7 @@ describe Gitlab::ImportExport::Group::TreeSaver do
create(:group_badge, group: group)
group_label = create(:group_label, group: group)
create(:label_priority, label: group_label, priority: 1)
- board = create(:board, group: group)
+ board = create(:board, group: group, milestone_id: Milestone::Upcoming.id)
create(:list, board: board, label: group_label)
create(:group_badge, group: group)
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 04587ef4240..de79156562d 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -3,6 +3,18 @@
require 'spec_helper'
describe Milestone do
+ describe 'MilestoneStruct#serializable_hash' do
+ let(:predefined_milestone) { described_class::MilestoneStruct.new('Test Milestone', '#test', 1) }
+
+ it 'presents the predefined milestone as a hash' do
+ expect(predefined_milestone.serializable_hash).to eq(
+ title: predefined_milestone.title,
+ name: predefined_milestone.name,
+ id: predefined_milestone.id
+ )
+ end
+ end
+
describe 'modules' do
context 'with a project' do
it_behaves_like 'AtomicInternalId' do
@@ -179,6 +191,16 @@ describe Milestone do
end
end
+ describe '.predefined_id?' do
+ it 'returns true for a predefined Milestone ID' do
+ expect(Milestone.predefined_id?(described_class::Upcoming.id)).to be true
+ end
+
+ it 'returns false for a Milestone ID that is not predefined' do
+ expect(Milestone.predefined_id?(milestone.id)).to be false
+ end
+ end
+
describe '.order_by_name_asc' do
it 'sorts by name ascending' do
milestone1 = create(:milestone, title: 'Foo')
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 93bc42c144d..1265b95736d 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -536,7 +536,7 @@ describe Snippet do
end
describe '#track_snippet_repository' do
- let(:snippet) { create(:snippet, :repository) }
+ let(:snippet) { create(:snippet) }
context 'when a snippet repository entry does not exist' do
it 'creates a new entry' do
@@ -554,7 +554,8 @@ describe Snippet do
end
context 'when a tracking entry exists' do
- let!(:snippet_repository) { create(:snippet_repository, snippet: snippet) }
+ let!(:snippet) { create(:snippet, :repository) }
+ let(:snippet_repository) { snippet.snippet_repository }
let!(:shard) { create(:shard, name: 'foo') }
it 'does not create a new entry in the database' do
@@ -592,7 +593,7 @@ describe Snippet do
end
context 'when repository exists' do
- let(:snippet) { create(:snippet, :repository) }
+ let!(:snippet) { create(:snippet, :repository) }
it 'does not try to create repository' do
expect(snippet.repository).not_to receive(:after_create)
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 21a65f361a9..58c40d04fe9 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -124,7 +124,7 @@ describe Projects::DestroyService do
allow(project.repository).to receive(:before_delete).and_raise(::Gitlab::Git::CommandError)
allow(Gitlab::GitLogger).to receive(:warn).with(
class: Repositories::DestroyService.name,
- project_id: project.id,
+ container_id: project.id,
disk_path: project.disk_path,
message: 'Gitlab::Git::CommandError').and_call_original
end
@@ -338,6 +338,39 @@ describe Projects::DestroyService do
end
end
+ context 'snippets' do
+ let!(:snippet1) { create(:project_snippet, project: project, author: user) }
+ let!(:snippet2) { create(:project_snippet, project: project, author: user) }
+
+ it 'does not include snippets when deleting in batches' do
+ expect(project).to receive(:destroy_dependent_associations_in_batches).with({ exclude: [:container_repositories, :snippets] })
+
+ destroy_project(project, user)
+ end
+
+ it 'calls the bulk snippet destroy service' do
+ expect(project.snippets.count).to eq 2
+
+ expect(Snippets::BulkDestroyService).to receive(:new)
+ .with(user, project.snippets).and_call_original
+
+ expect do
+ destroy_project(project, user)
+ end.to change(Snippet, :count).by(-2)
+ end
+
+ context 'when an error is raised deleting snippets' do
+ it 'does not delete project' do
+ allow_next_instance_of(Snippets::BulkDestroyService) do |instance|
+ allow(instance).to receive(:execute).and_return(ServiceResponse.error(message: 'foo'))
+ end
+
+ expect(destroy_project(project, user)).to be_falsey
+ expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_truthy
+ end
+ end
+ end
+
def destroy_project(project, user, params = {})
described_class.new(project, user, params).public_send(async ? :async_execute : :execute)
end
diff --git a/spec/services/snippets/bulk_destroy_service_spec.rb b/spec/services/snippets/bulk_destroy_service_spec.rb
new file mode 100644
index 00000000000..f03d7496f94
--- /dev/null
+++ b/spec/services/snippets/bulk_destroy_service_spec.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Snippets::BulkDestroyService do
+ let_it_be(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let!(:personal_snippet) { create(:personal_snippet, :repository, author: user) }
+ let!(:project_snippet) { create(:project_snippet, :repository, project: project, author: user) }
+ let(:snippets) { user.snippets }
+ let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:service_user) { user }
+
+ before do
+ project.add_developer(user)
+ end
+
+ subject { described_class.new(service_user, snippets) }
+
+ describe '#execute' do
+ it 'deletes the snippets in bulk' do
+ response = nil
+
+ expect(Repositories::ShellDestroyService).to receive(:new).with(personal_snippet.repository).and_call_original
+ expect(Repositories::ShellDestroyService).to receive(:new).with(project_snippet.repository).and_call_original
+
+ aggregate_failures do
+ expect do
+ response = subject.execute
+ end.to change(Snippet, :count).by(-2)
+
+ expect(response).to be_success
+ expect(repository_exists?(personal_snippet)).to be_falsey
+ expect(repository_exists?(project_snippet)).to be_falsey
+ end
+ end
+
+ context 'when snippets is empty' do
+ let(:snippets) { Snippet.none }
+
+ it 'returns a ServiceResponse success response' do
+ response = subject.execute
+
+ expect(response).to be_success
+ expect(response.message).to eq 'No snippets found.'
+ end
+ end
+
+ shared_examples 'error is raised' do
+ it 'returns error' do
+ response = subject.execute
+
+ aggregate_failures do
+ expect(response).to be_error
+ expect(response.message).to eq error_message
+ end
+ end
+
+ it 'no record is deleted' do
+ expect do
+ subject.execute
+ end.not_to change(Snippet, :count)
+ end
+ end
+
+ context 'when user does not have access to remove the snippet' do
+ let(:service_user) { create(:user) }
+
+ it_behaves_like 'error is raised' do
+ let(:error_message) { "You don't have access to delete these snippets." }
+ end
+ end
+
+ context 'when an error is raised deleting the repository' do
+ before do
+ allow_next_instance_of(Repositories::DestroyService) do |instance|
+ allow(instance).to receive(:execute).and_return({ status: :error })
+ end
+ end
+
+ it_behaves_like 'error is raised' do
+ let(:error_message) { 'Failed to delete snippet repositories.' }
+ end
+
+ it 'tries to rollback the repository' do
+ expect(subject).to receive(:attempt_rollback_repositories)
+
+ subject.execute
+ end
+ end
+
+ context 'when an error is raised deleting the records' do
+ before do
+ allow(snippets).to receive(:destroy_all).and_raise(ActiveRecord::ActiveRecordError)
+ end
+
+ it_behaves_like 'error is raised' do
+ let(:error_message) { 'Failed to remove snippets.' }
+ end
+
+ it 'tries to rollback the repository' do
+ expect(subject).to receive(:attempt_rollback_repositories)
+
+ subject.execute
+ end
+ end
+
+ context 'when snippet does not have a repository attached' do
+ let!(:snippet_without_repo) { create(:personal_snippet, author: user) }
+
+ it 'does not schedule anything for the snippet without repository and return success' do
+ response = nil
+
+ expect(Repositories::ShellDestroyService).to receive(:new).with(personal_snippet.repository).and_call_original
+ expect(Repositories::ShellDestroyService).to receive(:new).with(project_snippet.repository).and_call_original
+
+ expect do
+ response = subject.execute
+ end.to change(Snippet, :count).by(-3)
+
+ expect(response).to be_success
+ end
+ end
+ end
+
+ describe '#attempt_rollback_repositories' do
+ before do
+ Repositories::DestroyService.new(personal_snippet.repository).execute
+ end
+
+ it 'rollbacks the repository' do
+ error_msg = personal_snippet.disk_path + "+#{personal_snippet.id}+deleted.git"
+ expect(repository_exists?(personal_snippet, error_msg)).to be_truthy
+
+ subject.__send__(:attempt_rollback_repositories)
+
+ aggregate_failures do
+ expect(repository_exists?(personal_snippet, error_msg)).to be_falsey
+ expect(repository_exists?(personal_snippet)).to be_truthy
+ end
+ end
+
+ context 'when an error is raised' do
+ before do
+ allow_next_instance_of(Repositories::DestroyRollbackService) do |instance|
+ allow(instance).to receive(:execute).and_return({ status: :error })
+ end
+ end
+
+ it 'logs the error' do
+ expect(Gitlab::AppLogger).to receive(:error).with(/\ARepository .* in path .* could not be rolled back\z/).twice
+
+ subject.__send__(:attempt_rollback_repositories)
+ end
+ end
+ end
+
+ def repository_exists?(snippet, path = snippet.disk_path + ".git")
+ gitlab_shell.repository_exists?(snippet.snippet_repository.shard_name, path)
+ end
+end
diff --git a/spec/services/snippets/create_service_spec.rb b/spec/services/snippets/create_service_spec.rb
index a1cbec6748a..37b203c2341 100644
--- a/spec/services/snippets/create_service_spec.rb
+++ b/spec/services/snippets/create_service_spec.rb
@@ -18,7 +18,7 @@ describe Snippets::CreateService do
let(:extra_opts) { {} }
let(:creator) { admin }
- subject { Snippets::CreateService.new(project, creator, opts).execute }
+ subject { described_class.new(project, creator, opts).execute }
let(:snippet) { subject.payload[:snippet] }
diff --git a/spec/services/snippets/destroy_service_spec.rb b/spec/services/snippets/destroy_service_spec.rb
index bb035d275ab..840dc11a740 100644
--- a/spec/services/snippets/destroy_service_spec.rb
+++ b/spec/services/snippets/destroy_service_spec.rb
@@ -8,7 +8,7 @@ describe Snippets::DestroyService do
let_it_be(:other_user) { create(:user) }
describe '#execute' do
- subject { Snippets::DestroyService.new(user, snippet).execute }
+ subject { described_class.new(user, snippet).execute }
context 'when snippet is nil' do
let(:snippet) { nil }
@@ -30,7 +30,7 @@ describe Snippets::DestroyService do
shared_examples 'an unsuccessful destroy' do
it 'does not delete the snippet' do
- expect { subject }.to change { Snippet.count }.by(0)
+ expect { subject }.not_to change { Snippet.count }
end
it 'returns ServiceResponse error' do
@@ -38,8 +38,63 @@ describe Snippets::DestroyService do
end
end
+ shared_examples 'deletes the snippet repository' do
+ it 'removes the snippet repository' do
+ expect(snippet.repository.exists?).to be_truthy
+ expect(GitlabShellWorker).to receive(:perform_in)
+ expect_next_instance_of(Repositories::DestroyService) do |instance|
+ expect(instance).to receive(:execute).and_call_original
+ end
+
+ expect(subject).to be_success
+ end
+
+ context 'when the repository deletion service raises an error' do
+ before do
+ allow_next_instance_of(Repositories::DestroyService) do |instance|
+ allow(instance).to receive(:execute).and_return({ status: :error })
+ end
+ end
+
+ it_behaves_like 'an unsuccessful destroy'
+
+ it 'does not try to rollback repository' do
+ expect(Repositories::DestroyRollbackService).not_to receive(:new)
+
+ subject
+ end
+ end
+
+ context 'when a destroy error is raised' do
+ before do
+ allow(snippet).to receive(:destroy!).and_raise(ActiveRecord::ActiveRecordError)
+ end
+
+ it_behaves_like 'an unsuccessful destroy'
+
+ it 'attempts to rollback the repository' do
+ expect(Repositories::DestroyRollbackService).to receive(:new).and_call_original
+
+ subject
+ end
+ end
+
+ context 'when repository is nil' do
+ it 'does not schedule anything and return success' do
+ allow(snippet).to receive(:repository).and_return(nil)
+
+ expect(GitlabShellWorker).not_to receive(:perform_in)
+ expect_next_instance_of(Repositories::DestroyService) do |instance|
+ expect(instance).to receive(:execute).and_call_original
+ end
+
+ expect(subject).to be_success
+ end
+ end
+ end
+
context 'when ProjectSnippet' do
- let!(:snippet) { create(:project_snippet, project: project, author: author) }
+ let!(:snippet) { create(:project_snippet, :repository, project: project, author: author) }
context 'when user is able to admin_project_snippet' do
let(:author) { user }
@@ -49,6 +104,7 @@ describe Snippets::DestroyService do
end
it_behaves_like 'a successful destroy'
+ it_behaves_like 'deletes the snippet repository'
end
context 'when user is not able to admin_project_snippet' do
@@ -59,12 +115,13 @@ describe Snippets::DestroyService do
end
context 'when PersonalSnippet' do
- let!(:snippet) { create(:personal_snippet, author: author) }
+ let!(:snippet) { create(:personal_snippet, :repository, author: author) }
context 'when user is able to admin_personal_snippet' do
let(:author) { user }
it_behaves_like 'a successful destroy'
+ it_behaves_like 'deletes the snippet repository'
end
context 'when user is not able to admin_personal_snippet' do
@@ -73,5 +130,21 @@ describe Snippets::DestroyService do
it_behaves_like 'an unsuccessful destroy'
end
end
+
+ context 'when the repository does not exists' do
+ let(:snippet) { create(:personal_snippet, author: user) }
+
+ it 'does not schedule anything and return success' do
+ expect(snippet.repository).not_to be_nil
+ expect(snippet.repository.exists?).to be_falsey
+
+ expect(GitlabShellWorker).not_to receive(:perform_in)
+ expect_next_instance_of(Repositories::DestroyService) do |instance|
+ expect(instance).to receive(:execute).and_call_original
+ end
+
+ expect(subject).to be_success
+ end
+ end
end
end
diff --git a/spec/services/snippets/update_service_spec.rb b/spec/services/snippets/update_service_spec.rb
index b8215f9779d..4858a0512ad 100644
--- a/spec/services/snippets/update_service_spec.rb
+++ b/spec/services/snippets/update_service_spec.rb
@@ -18,7 +18,7 @@ describe Snippets::UpdateService do
let(:updater) { user }
subject do
- Snippets::UpdateService.new(
+ described_class.new(
project,
updater,
options
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 2b658a93b0a..a664719783a 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -26,6 +26,12 @@ describe Users::DestroyService do
service.execute(user)
end
+ it 'does not include snippets when deleting in batches' do
+ expect(user).to receive(:destroy_dependent_associations_in_batches).with({ exclude: [:snippets] })
+
+ service.execute(user)
+ end
+
it 'will delete the project' do
expect_next_instance_of(Projects::DestroyService) do |destroy_service|
expect(destroy_service).to receive(:execute).once.and_return(true)
@@ -33,6 +39,54 @@ describe Users::DestroyService do
service.execute(user)
end
+
+ it 'calls the bulk snippet destroy service for the user personal snippets' do
+ repo1 = create(:personal_snippet, :repository, author: user).snippet_repository
+ repo2 = create(:project_snippet, :repository, author: user).snippet_repository
+ repo3 = create(:project_snippet, :repository, project: project, author: user).snippet_repository
+
+ aggregate_failures do
+ expect(gitlab_shell.repository_exists?(repo1.shard_name, repo1.disk_path + '.git')).to be_truthy
+ expect(gitlab_shell.repository_exists?(repo2.shard_name, repo2.disk_path + '.git')).to be_truthy
+ expect(gitlab_shell.repository_exists?(repo3.shard_name, repo3.disk_path + '.git')).to be_truthy
+ end
+
+ # Call made when destroying user personal projects
+ expect(Snippets::BulkDestroyService).to receive(:new)
+ .with(admin, project.snippets).and_call_original
+
+ # Call to remove user personal snippets and for
+ # project snippets where projects are not user personal
+ # ones
+ expect(Snippets::BulkDestroyService).to receive(:new)
+ .with(admin, user.snippets).and_call_original
+
+ service.execute(user)
+
+ aggregate_failures do
+ expect(gitlab_shell.repository_exists?(repo1.shard_name, repo1.disk_path + '.git')).to be_falsey
+ expect(gitlab_shell.repository_exists?(repo2.shard_name, repo2.disk_path + '.git')).to be_falsey
+ expect(gitlab_shell.repository_exists?(repo3.shard_name, repo3.disk_path + '.git')).to be_falsey
+ end
+ end
+
+ context 'when an error is raised deleting snippets' do
+ it 'does not delete user' do
+ snippet = create(:personal_snippet, :repository, author: user)
+
+ bulk_service = double
+ allow(Snippets::BulkDestroyService).to receive(:new).and_call_original
+ allow(Snippets::BulkDestroyService).to receive(:new).with(admin, user.snippets).and_return(bulk_service)
+ allow(bulk_service).to receive(:execute).and_return(ServiceResponse.error(message: 'foo'))
+
+ aggregate_failures do
+ expect { service.execute(user) }
+ .to raise_error(Users::DestroyService::DestroyError, 'foo' )
+ expect(snippet.reload).not_to be_nil
+ expect(gitlab_shell.repository_exists?(snippet.repository_storage, snippet.disk_path + '.git')).to be_truthy
+ end
+ end
+ end
end
context 'projects in pending_delete' do