diff options
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 Binary files differnew file mode 100644 index 00000000000..dfda1deb649 --- /dev/null +++ b/doc/ci/img/environments_deployment_cluster_v12_8.png 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 |