Commit f54a50aa authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent c77fda90
<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"
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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)
......
......@@ -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
......
# 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
......@@ -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
attempt_destroy!
ServiceResponse.success(message: 'Snippet was deleted.')
else
rescue DestroyError
service_response_error('Failed to remove snippet repository.', 400)
rescue
attempt_rollback_repository
service_response_error('Failed to remove snippet.', 400)
end
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)
......
......@@ -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
......
......@@ -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?
......
---
title: Add/update services to delete snippets repositories
merge_request: 22672
author:
type: added
---
title: Update moved service desk issues notifications
merge_request: 25640
author:
type: added
---
title: Migrated from .fa-spinner to .spinner in app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
merge_request: 24919
author: rk4bir
type: changed
......@@ -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
```
......@@ -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
......
......@@ -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
```
......
......@@ -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
```
......
......@@ -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
```
......
......@@ -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
......
......@@ -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
......
......@@ -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>
```
......
......@@ -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
```
......
......@@ -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
```
......
......@@ -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"
```
......
......@@ -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"
```
......@@ -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
```
......
......@@ -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
```
......
......@@ -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"
```
......@@ -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
```
......
......@@ -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
```
......
......@@ -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):
......
......@@ -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
```
......
......@@ -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
```
......
......@@ -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
......
......@@ -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
```
......
......@@ -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).
......
......@@ -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.
......
......@@ -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
......
......@@ -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.
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -70,6 +70,7 @@ ee:
- :push_event_payload
- boards:
- :board_assignee
- :milestone
- labels:
- :priorities
- lists:
......
......@@ -188,6 +188,7 @@ excluded_attributes:
issues:
- :milestone_id
- :moved_to_id
- :sent_notifications
- :state_id
- :duplicated_to_id
- :promoted_to_epic_id
......
......@@ -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 ""
......
......@@ -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
......
......@@ -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
......
import Vue from 'vue';
import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue';
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 component;
let AssigneeTitleComponent;
let wrapper;
beforeEach(() => {
AssigneeTitleComponent = Vue.extend(AssigneeTitle);
const createComponent = props => {
return shallowMount(Component, {
propsData: {
numberOfAssignees: 0,
editable: false,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('assignee title', () => {
it('renders assignee', () => {
component = new AssigneeTitleComponent({
propsData: {
wrapper = createComponent({
numberOfAssignees: 1,
editable: false,
},
}).$mount();
});
expect(component.$el.innerText.trim()).toEqual('Assignee');
expect(wrapper.vm.$el.innerText.trim()).toEqual('Assignee');
});
it('renders 2 assignees', () => {
component = new AssigneeTitleComponent({
propsData: {
wrapper = createComponent({
numberOfAssignees: 2,
editable: false,
},
}).$mount();
});
expect(component.$el.innerText.trim()).toEqual('2 Assignees');
expect(wrapper.vm.$el.innerText.trim()).toEqual('2 Assignees');
});
});
describe('gutter toggle', () => {
it('does not show toggle by default', () => {
component = new AssigneeTitleComponent({
propsData: {
wrapper = createComponent({
numberOfAssignees: 2,
editable: false,
},
}).$mount();
});
expect(component.$el.querySelector('.gutter-toggle')).toBeNull();
expect(wrapper.vm.$el.querySelector('.gutter-toggle')).toBeNull();
});
it('shows toggle when showToggle is true', () => {
component = new AssigneeTitleComponent({
propsData: {
wrapper = createComponent({
numberOfAssignees: 2,
editable: false,
showToggle: true,
},
}).$mount();
});
expect(component.$el.querySelector('.gutter-toggle')).toEqual(jasmine.any(Object));
expect(wrapper.vm.$el.querySelector('.gutter-toggle')).toEqual(expect.any(Object));
});
});
it('does not render spinner by default', () => {
component = new AssigneeTitleComponent({
propsData: {
wrapper = createComponent({
numberOfAssignees: 0,
editable: false,
},
}).$mount();
});
expect(component.$el.querySelector('.fa')).toBeNull();
expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
});
it('renders spinner when loading', () => {
component = new AssigneeTitleComponent({
propsData: {
wrapper = createComponent({
loading: true,
numberOfAssignees: 0,
editable: false,
},
}).$mount();
});
expect(component.$el.querySelector('.fa')).not.toBeNull();
expect(wrapper.find(GlLoadingIcon).exists()).toBeTruthy();
});
it('does not render edit link when not editable', () => {
component = new AssigneeTitleComponent({
propsData: {
wrapper = createComponent({
numberOfAssignees: 0,
editable: false,
},
}).$mount();
});
expect(component.$el.querySelector('.edit-link')).toBeNull();
expect(wrapper.vm.$el.querySelector('.edit-link')).toBeNull();
});
it('renders edit link when editable', () => {
component = new AssigneeTitleComponent({
propsData: {
wrapper = createComponent({
numberOfAssignees: 0,
editable: true,
},
}).$mount();
});
expect(component.$el.querySelector('.edit-link')).not.toBeNull();
expect(wrapper.vm.$el.querySelector('.edit-link')).not.toBeNull();
});
it('tracks the event when edit is clicked', () => {
component = new AssigneeTitleComponent({
propsData: {
wrapper = createComponent({
numberOfAssignees: 0,
editable: true,
},
}).$mount();
});
const spy = mockTracking('_category_', component.$el, spyOn);
const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
triggerEvent('.js-sidebar-dropdown-toggle');
expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
......
......@@ -10,6 +10,7 @@ issues:
- resource_label_events
- resource_weight_events
- resource_milestone_events
- sent_notifications
- sentry_issue
- label_links
- labels
......
......@@ -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)
......
......@@ -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)
......
......@@ -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')
......
......@@ -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)
......
......@@ -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
......
# 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
......@@ -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] }
......
......@@ -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
......@@ -18,7 +18,7 @@ describe Snippets::UpdateService do
let(:updater) { user }
subject do
Snippets::UpdateService.new(
described_class.new(
project,
updater,
options
......
......@@ -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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment