Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b6724a211e
commit
419f9c0ac3
52 changed files with 516 additions and 145 deletions
|
@ -1144,15 +1144,6 @@ Rails/SaveBang:
|
|||
- 'spec/support/shared_contexts/email_shared_context.rb'
|
||||
- 'spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb'
|
||||
- 'spec/support/shared_contexts/mailers/notify_shared_context.rb'
|
||||
- 'spec/support/shared_examples/controllers/cache_control_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/features/editable_merge_request_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/policies/project_policy_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/serializers/note_entity_shared_examples.rb'
|
||||
- 'spec/tasks/gitlab/web_hook_rake_spec.rb'
|
||||
- 'spec/uploaders/file_uploader_spec.rb'
|
||||
- 'spec/uploaders/object_storage_spec.rb'
|
||||
|
|
|
@ -40,7 +40,7 @@ export default {
|
|||
},
|
||||
inject: {
|
||||
boardId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
|
|
@ -58,7 +58,7 @@ export default {
|
|||
},
|
||||
inject: {
|
||||
boardId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
|
|
@ -22,11 +22,7 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
inject: {
|
||||
groupId: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
inject: ['groupId'],
|
||||
data() {
|
||||
return {
|
||||
title: '',
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { __ } from '../../../../locale';
|
||||
import tooltip from '../../../../vue_shared/directives/tooltip';
|
||||
|
||||
const directions = {
|
||||
up: 'up',
|
||||
|
@ -10,7 +9,7 @@ const directions = {
|
|||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
components: {
|
||||
GlIcon,
|
||||
|
@ -46,7 +45,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div
|
||||
v-tooltip
|
||||
v-gl-tooltip
|
||||
:title="tooltipTitle"
|
||||
class="controllers-buttons"
|
||||
data-container="body"
|
||||
|
|
|
@ -19,7 +19,6 @@ import Api from '~/api';
|
|||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
||||
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { convertToSnakeCase } from '~/lib/utils/text_utility';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { urlParamsToObject } from '~/lib/utils/common_utils';
|
||||
|
@ -113,7 +112,6 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
inject: [
|
||||
'projectPath',
|
||||
'newIssuePath',
|
||||
|
@ -335,10 +333,7 @@ export default {
|
|||
return Boolean(assignees.nodes?.length);
|
||||
},
|
||||
navigateToIncidentDetails({ iid }) {
|
||||
const path = this.glFeatures.issuesIncidentDetails
|
||||
? joinPaths(this.issuePath, INCIDENT_DETAILS_PATH)
|
||||
: this.issuePath;
|
||||
return visitUrl(joinPaths(path, iid));
|
||||
return visitUrl(joinPaths(this.issuePath, INCIDENT_DETAILS_PATH, iid));
|
||||
},
|
||||
handlePageChange(page) {
|
||||
const { startCursor, endCursor } = this.incidents.pageInfo;
|
||||
|
|
|
@ -36,7 +36,7 @@ class Admin::HooksController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def destroy
|
||||
hook.destroy
|
||||
destroy_hook(hook)
|
||||
|
||||
redirect_to admin_hooks_path, status: :found
|
||||
end
|
||||
|
|
|
@ -5,6 +5,21 @@ module HooksExecution
|
|||
|
||||
private
|
||||
|
||||
def destroy_hook(hook)
|
||||
result = WebHooks::DestroyService.new(current_user).execute(hook)
|
||||
|
||||
if result[:status] == :success
|
||||
flash[:notice] =
|
||||
if result[:async]
|
||||
_("%{hook_type} was scheduled for deletion") % { hook_type: hook.model_name.human }
|
||||
else
|
||||
_("%{hook_type} was deleted") % { hook_type: hook.model_name.human }
|
||||
end
|
||||
else
|
||||
flash[:alert] = result[:message]
|
||||
end
|
||||
end
|
||||
|
||||
def set_hook_execution_notice(result)
|
||||
http_status = result[:http_status]
|
||||
message = result[:message]
|
||||
|
|
|
@ -50,7 +50,7 @@ class Projects::HooksController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def destroy
|
||||
hook.destroy
|
||||
destroy_hook(hook)
|
||||
|
||||
redirect_to action: :index, status: :found
|
||||
end
|
||||
|
|
|
@ -5,13 +5,8 @@ class Projects::IncidentsController < Projects::ApplicationController
|
|||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
before_action :authorize_read_issue!
|
||||
before_action :check_feature_flag, only: [:show]
|
||||
before_action :load_incident, only: [:show]
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:issues_incident_details, @project)
|
||||
end
|
||||
|
||||
def index
|
||||
end
|
||||
|
||||
|
@ -45,8 +40,4 @@ class Projects::IncidentsController < Projects::ApplicationController
|
|||
def serializer
|
||||
IssueSerializer.new(current_user: current_user, project: incident.project)
|
||||
end
|
||||
|
||||
def check_feature_flag
|
||||
render_404 unless Feature.enabled?(:issues_incident_details, @project)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,8 +2,14 @@
|
|||
|
||||
module Types
|
||||
class PackageTypeEnum < BaseEnum
|
||||
PACKAGE_TYPE_NAMES = {
|
||||
pypi: 'PyPI',
|
||||
npm: 'NPM'
|
||||
}.freeze
|
||||
|
||||
::Packages::Package.package_types.keys.each do |package_type|
|
||||
value package_type.to_s.upcase, "Packages from the #{package_type.capitalize} package manager", value: package_type.to_s
|
||||
type_name = PACKAGE_TYPE_NAMES.fetch(package_type.to_sym, package_type.capitalize)
|
||||
value package_type.to_s.upcase, "Packages from the #{type_name} package manager", value: package_type.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,11 +21,6 @@ module AlertManagement
|
|||
ignored: 3
|
||||
}.freeze
|
||||
|
||||
OPEN_STATUSES = [
|
||||
:triggered,
|
||||
:acknowledged
|
||||
].freeze
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :issue, optional: true
|
||||
belongs_to :prometheus_alert, optional: true
|
||||
|
@ -118,7 +113,7 @@ module AlertManagement
|
|||
scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) }
|
||||
scope :for_environment, -> (environment) { where(environment: environment) }
|
||||
scope :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) }
|
||||
scope :open, -> { with_status(OPEN_STATUSES) }
|
||||
scope :open, -> { with_status(open_statuses) }
|
||||
scope :not_resolved, -> { without_status(:resolved) }
|
||||
scope :with_prometheus_alert, -> { includes(:prometheus_alert) }
|
||||
|
||||
|
@ -183,6 +178,14 @@ module AlertManagement
|
|||
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
|
||||
end
|
||||
|
||||
def self.open_statuses
|
||||
[:triggered, :acknowledged]
|
||||
end
|
||||
|
||||
def self.open_status?(status)
|
||||
open_statuses.include?(status)
|
||||
end
|
||||
|
||||
def status_event_for(status)
|
||||
self.class.state_machines[:status].events.transitions_for(self, to: status.to_s.to_sym).first&.event
|
||||
end
|
||||
|
|
|
@ -144,7 +144,7 @@ module AlertManagement
|
|||
|
||||
def filter_duplicate
|
||||
# Only need to check if changing to an open status
|
||||
return unless params[:status_event] && AlertManagement::Alert::OPEN_STATUSES.include?(status_key)
|
||||
return unless params[:status_event] && AlertManagement::Alert.open_status?(status_key)
|
||||
|
||||
param_errors << unresolved_alert_error if duplicate_alert?
|
||||
end
|
||||
|
|
78
app/services/web_hooks/destroy_service.rb
Normal file
78
app/services/web_hooks/destroy_service.rb
Normal file
|
@ -0,0 +1,78 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WebHooks
|
||||
class DestroyService
|
||||
include BaseServiceUtility
|
||||
|
||||
BATCH_SIZE = 1000
|
||||
LOG_COUNT_THRESHOLD = 10000
|
||||
|
||||
DestroyError = Class.new(StandardError)
|
||||
|
||||
attr_accessor :current_user, :web_hook
|
||||
|
||||
def initialize(current_user)
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
def execute(web_hook)
|
||||
@web_hook = web_hook
|
||||
|
||||
async = false
|
||||
# For a better user experience, it's better if the Web hook is
|
||||
# destroyed right away without waiting for Sidekiq. However, if
|
||||
# there are a lot of Web hook logs, we will need more time to
|
||||
# clean them up, so schedule a Sidekiq job to do this.
|
||||
if needs_async_destroy?
|
||||
Gitlab::AppLogger.info("User #{current_user&.id} scheduled a deletion of hook ID #{web_hook.id}")
|
||||
async_destroy(web_hook)
|
||||
async = true
|
||||
else
|
||||
sync_destroy(web_hook)
|
||||
end
|
||||
|
||||
success({ async: async })
|
||||
end
|
||||
|
||||
def sync_destroy(web_hook)
|
||||
@web_hook = web_hook
|
||||
|
||||
delete_web_hook_logs
|
||||
result = web_hook.destroy
|
||||
|
||||
if result
|
||||
success({ async: false })
|
||||
else
|
||||
error("Unable to destroy #{web_hook.model_name.human}")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def async_destroy(web_hook)
|
||||
WebHooks::DestroyWorker.perform_async(current_user.id, web_hook.id)
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def needs_async_destroy?
|
||||
web_hook.web_hook_logs.limit(LOG_COUNT_THRESHOLD).count == LOG_COUNT_THRESHOLD
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def delete_web_hook_logs
|
||||
loop do
|
||||
count = delete_web_hook_logs_in_batches
|
||||
break if count < BATCH_SIZE
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def delete_web_hook_logs_in_batches
|
||||
# We can't use EachBatch because that does an ORDER BY id, which can
|
||||
# easily time out. We don't actually care about ordering when
|
||||
# we are deleting these rows.
|
||||
web_hook.web_hook_logs.limit(BATCH_SIZE).delete_all
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
|
@ -1940,6 +1940,14 @@
|
|||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: web_hooks_destroy
|
||||
:feature_category: :integrations
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: x509_certificate_revoke
|
||||
:feature_category: :source_code_management
|
||||
:has_external_dependencies:
|
||||
|
|
27
app/workers/web_hooks/destroy_worker.rb
Normal file
27
app/workers/web_hooks/destroy_worker.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WebHooks
|
||||
class DestroyWorker
|
||||
include ApplicationWorker
|
||||
|
||||
feature_category :integrations
|
||||
urgency :low
|
||||
idempotent!
|
||||
|
||||
def perform(user_id, web_hook_id)
|
||||
user = User.find_by_id(user_id)
|
||||
hook = WebHook.find_by_id(web_hook_id)
|
||||
|
||||
return unless user && hook
|
||||
|
||||
result = ::WebHooks::DestroyService.new(user).sync_destroy(hook)
|
||||
|
||||
return result if result[:status] == :success
|
||||
|
||||
e = ::WebHooks::DestroyService::DestroyError.new(result[:message])
|
||||
Gitlab::ErrorTracking.track_exception(e, web_hook_id: hook.id)
|
||||
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow users to navigate to the incidents show details page wrapper through `/issues/incidents/:id` from the Incident list
|
||||
merge_request: 44438
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/mb-rails-save-bang-rubcop.yml
Normal file
5
changelogs/unreleased/mb-rails-save-bang-rubcop.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Rails/SaveBang offenses for spec files in spec/support/shared_example/*
|
||||
merge_request: 44424
|
||||
author: matthewbried
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Web hook deletion not working when many hook logs are present
|
||||
merge_request: 43464
|
||||
author:
|
||||
type: fixed
|
|
@ -128,12 +128,12 @@ module Gitlab
|
|||
/^description$/,
|
||||
/^note$/,
|
||||
/^text$/,
|
||||
/^title$/
|
||||
/^title$/,
|
||||
/^hook$/
|
||||
]
|
||||
config.filter_parameters += %i(
|
||||
certificate
|
||||
encrypted_key
|
||||
hook
|
||||
import_url
|
||||
elasticsearch_url
|
||||
otp_attempt
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: ci_instance_variables_ui
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33510
|
||||
rollout_issue_url:
|
||||
group: group::continuous integration
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: issues_incident_details
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43459
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/257842
|
||||
type: development
|
||||
group: group::health
|
||||
default_enabled: false
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: merge_request_draft_filter
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35942
|
||||
rollout_issue_url:
|
||||
group: group::source code
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: new_variables_ui
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25260
|
||||
rollout_issue_url:
|
||||
group: group::continuous integration
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
@ -310,5 +310,7 @@
|
|||
- 1
|
||||
- - web_hook
|
||||
- 1
|
||||
- - web_hooks_destroy
|
||||
- 1
|
||||
- - x509_certificate_revoke
|
||||
- 1
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
# rubocop:disable Style/SignalException
|
||||
|
||||
PATTERNS = %w[
|
||||
createFlash
|
||||
|
@ -12,6 +13,9 @@ PATTERNS = %w[
|
|||
initDeprecatedJQueryDropdown
|
||||
].freeze
|
||||
|
||||
BLOCKING_PATTERNS = %w[
|
||||
].freeze
|
||||
|
||||
def get_added_lines(files)
|
||||
lines = []
|
||||
files.each do |file|
|
||||
|
@ -25,19 +29,34 @@ changed_vue_haml_files = helper.changed_files(/.vue$|.haml$/)
|
|||
return if changed_vue_haml_files.empty?
|
||||
|
||||
changed_lines_in_mr = get_added_lines(changed_vue_haml_files)
|
||||
has_deprecated_components = changed_lines_in_mr.select { |i| i[/#{PATTERNS.join("|")}/] }
|
||||
deprecated_components_in_mr = PATTERNS.select { |s| has_deprecated_components.join(" ")[s] }
|
||||
deprecated_components_in_mr = PATTERNS.select { |pattern| changed_lines_in_mr.any? { |line| line[pattern] } }
|
||||
blocking_components_in_mr = BLOCKING_PATTERNS.select { |pattern| changed_lines_in_mr.any? { |line| line[pattern] } }
|
||||
|
||||
return if deprecated_components_in_mr.empty?
|
||||
|
||||
warn "This merge request contains deprecated components. Please consider using Pajamas components instead."
|
||||
return if (deprecated_components_in_mr + blocking_components_in_mr).empty?
|
||||
|
||||
markdown(<<~MARKDOWN)
|
||||
## Deprecated components
|
||||
|
||||
The following components are deprecated:
|
||||
|
||||
* #{deprecated_components_in_mr.join("\n* ")}
|
||||
|
||||
Please consider using [Pajamas components](https://design.gitlab.com/components/status/) instead.
|
||||
MARKDOWN
|
||||
|
||||
if blocking_components_in_mr.any?
|
||||
markdown(<<~MARKDOWN)
|
||||
These deprecated components have already been migrated and can no longer be used. Please use [Pajamas components](https://design.gitlab.com/components/status/) instead.
|
||||
|
||||
* #{blocking_components_in_mr.join("\n* ")}
|
||||
|
||||
MARKDOWN
|
||||
|
||||
fail "This merge request contains deprecated components that have been migrated and can no longer be used. Please use Pajamas components instead."
|
||||
end
|
||||
|
||||
if deprecated_components_in_mr.any?
|
||||
markdown(<<~MARKDOWN)
|
||||
These deprecated components are in the process of being migrated. Please consider using [Pajamas components](https://design.gitlab.com/components/status/) instead.
|
||||
|
||||
* #{deprecated_components_in_mr.join("\n* ")}
|
||||
|
||||
MARKDOWN
|
||||
|
||||
warn "This merge request contains deprecated components. Please consider using Pajamas components instead."
|
||||
end
|
||||
|
|
|
@ -12464,7 +12464,7 @@ enum PackageTypeEnum {
|
|||
MAVEN
|
||||
|
||||
"""
|
||||
Packages from the Npm package manager
|
||||
Packages from the NPM package manager
|
||||
"""
|
||||
NPM
|
||||
|
||||
|
@ -12474,7 +12474,7 @@ enum PackageTypeEnum {
|
|||
NUGET
|
||||
|
||||
"""
|
||||
Packages from the Pypi package manager
|
||||
Packages from the PyPI package manager
|
||||
"""
|
||||
PYPI
|
||||
}
|
||||
|
|
|
@ -36751,7 +36751,7 @@
|
|||
},
|
||||
{
|
||||
"name": "NPM",
|
||||
"description": "Packages from the Npm package manager",
|
||||
"description": "Packages from the NPM package manager",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
|
@ -36769,7 +36769,7 @@
|
|||
},
|
||||
{
|
||||
"name": "PYPI",
|
||||
"description": "Packages from the Pypi package manager",
|
||||
"description": "Packages from the PyPI package manager",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
|
|
|
@ -3458,9 +3458,9 @@ Values for sorting projects.
|
|||
| `GENERIC` | Packages from the Generic package manager |
|
||||
| `GOLANG` | Packages from the Golang package manager |
|
||||
| `MAVEN` | Packages from the Maven package manager |
|
||||
| `NPM` | Packages from the Npm package manager |
|
||||
| `NPM` | Packages from the NPM package manager |
|
||||
| `NUGET` | Packages from the Nuget package manager |
|
||||
| `PYPI` | Packages from the Pypi package manager |
|
||||
| `PYPI` | Packages from the PyPI package manager |
|
||||
|
||||
### PipelineConfigSourceEnum
|
||||
|
||||
|
|
|
@ -7,11 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Repositories Analytics **(PREMIUM)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215104) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4.
|
||||
> - It's [deployed behind a feature flag](../../feature_flags.md), enabled by default.
|
||||
> - It's enabled on GitLab.com.
|
||||
> - It's recommended for production use.
|
||||
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-repositories-analytics). **(CORE ONLY)**
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215104) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4.
|
||||
|
||||
CAUTION: **Warning:**
|
||||
This feature might not be available to you. Check the **version history** note above for details.
|
||||
|
@ -35,25 +31,6 @@ For each day that a coverage report was generated by a job in a project's pipeli
|
|||
|
||||
If the project's code coverage was calculated more than once in a day, we will take the last value from that day.
|
||||
|
||||
## Enable or disable Repositories Analytics **(CORE ONLY)**
|
||||
|
||||
Repositories Analytics is under development but ready for production use.
|
||||
It is deployed behind a feature flag that is **enabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
|
||||
can opt to disable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:group_coverage_reports)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:group_coverage_reports)
|
||||
```
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||
|
|
|
@ -104,7 +104,9 @@ module API
|
|||
delete ":id/hooks/:hook_id" do
|
||||
hook = user_project.hooks.find(params.delete(:hook_id))
|
||||
|
||||
destroy_conditionally!(hook)
|
||||
destroy_conditionally!(hook) do
|
||||
WebHooks::DestroyService.new(current_user).execute(hook)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -70,7 +70,9 @@ module API
|
|||
hook = SystemHook.find_by(id: params[:id])
|
||||
not_found!('System hook') unless hook
|
||||
|
||||
destroy_conditionally!(hook)
|
||||
destroy_conditionally!(hook) do
|
||||
WebHooks::DestroyService.new(current_user).execute(hook)
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
|
|
@ -37,7 +37,10 @@ namespace :gitlab do
|
|||
web_hooks.find_each do |hook|
|
||||
next unless hook.url == web_hook_url
|
||||
|
||||
hook.destroy!
|
||||
result = WebHooks::DestroyService.new(nil).sync_destroy(hook)
|
||||
|
||||
raise "Unable to destroy Web hook" unless result[:status] == :success
|
||||
|
||||
count += 1
|
||||
end
|
||||
|
||||
|
|
|
@ -498,6 +498,12 @@ msgstr ""
|
|||
msgid "%{group_name}&%{epic_iid} · opened %{epic_created} by %{author}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{hook_type} was deleted"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{hook_type} was scheduled for deletion"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{host} sign-in from new location"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -29,4 +29,12 @@ RSpec.describe Admin::HooksController do
|
|||
expect(SystemHook.first).to have_attributes(hook_params)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
let!(:hook) { create(:system_hook) }
|
||||
let!(:log) { create(:web_hook_log, web_hook: hook) }
|
||||
let(:params) { { id: hook } }
|
||||
|
||||
it_behaves_like 'Web hook destroyer'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,6 +48,14 @@ RSpec.describe Projects::HooksController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
let!(:hook) { create(:project_hook, project: project) }
|
||||
let!(:log) { create(:web_hook_log, web_hook: hook) }
|
||||
let(:params) { { namespace_id: project.namespace, project_id: project, id: hook } }
|
||||
|
||||
it_behaves_like 'Web hook destroyer'
|
||||
end
|
||||
|
||||
describe '#test' do
|
||||
let(:hook) { create(:project_hook, project: project) }
|
||||
|
||||
|
|
|
@ -83,14 +83,6 @@ RSpec.describe Projects::IncidentsController do
|
|||
expect(assigns(:noteable)).to eq(assigns(:incident))
|
||||
end
|
||||
|
||||
context 'with feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(issues_incident_details: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'not found'
|
||||
end
|
||||
|
||||
context 'with non existing id' do
|
||||
let(:resource) { non_existing_record_id }
|
||||
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Merge Requests > User filters', :js do
|
||||
include FilteredSearchHelpers
|
||||
|
||||
let_it_be(:project) { create(:project, :public, :repository) }
|
||||
let_it_be(:user) { project.creator }
|
||||
let_it_be(:group_user) { create(:user) }
|
||||
let_it_be(:first_user) { create(:user) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
visit project_merge_requests_path(project)
|
||||
end
|
||||
|
||||
context 'by "approved by"' do
|
||||
let_it_be(:merge_request) { create(:merge_request, title: 'Bugfix3', source_project: project, source_branch: 'bugfix3') }
|
||||
|
||||
let_it_be(:merge_request_with_first_user_approval) do
|
||||
create(:merge_request, source_project: project, title: 'Bugfix5').tap do |mr|
|
||||
create(:approval, merge_request: mr, user: first_user)
|
||||
end
|
||||
end
|
||||
|
||||
let_it_be(:merge_request_with_group_user_approved) do
|
||||
group = create(:group)
|
||||
group.add_developer(group_user)
|
||||
|
||||
create(:merge_request, source_project: project, title: 'Bugfix6', source_branch: 'bugfix6').tap do |mr|
|
||||
create(:approval, merge_request: mr, user: group_user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'filtering by approved-by:none' do
|
||||
it 'applies the filter' do
|
||||
input_filtered_search('approved-by:=none')
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
|
||||
expect(page).not_to have_content 'Bugfix5'
|
||||
expect(page).not_to have_content 'Bugfix6'
|
||||
expect(page).to have_content 'Bugfix3'
|
||||
end
|
||||
end
|
||||
|
||||
context 'filtering by approved-by:any' do
|
||||
it 'applies the filter' do
|
||||
input_filtered_search('approved-by:=any')
|
||||
|
||||
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
|
||||
|
||||
expect(page).to have_content 'Bugfix5'
|
||||
expect(page).not_to have_content 'Bugfix3'
|
||||
end
|
||||
end
|
||||
|
||||
context 'filtering by approved-by:@username' do
|
||||
it 'applies the filter' do
|
||||
input_filtered_search("approved-by:=@#{first_user.username}")
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
|
||||
expect(page).to have_content 'Bugfix5'
|
||||
expect(page).not_to have_content 'Bugfix3'
|
||||
end
|
||||
end
|
||||
|
||||
context 'filtering by an approver from a group' do
|
||||
it 'applies the filter' do
|
||||
input_filtered_search("approved-by:=@#{group_user.username}")
|
||||
|
||||
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
|
||||
|
||||
expect(page).to have_content 'Bugfix6'
|
||||
expect(page).not_to have_content 'Bugfix5'
|
||||
expect(page).not_to have_content 'Bugfix3'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -31,7 +31,7 @@ describe('IDE job log scroll button', () => {
|
|||
});
|
||||
|
||||
it('returns proper title', () => {
|
||||
expect(wrapper.attributes('data-original-title')).toBe(title);
|
||||
expect(wrapper.attributes('title')).toBe(title);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -87,7 +87,6 @@ describe('Incidents List', () => {
|
|||
textQuery: '',
|
||||
authorUsernamesQuery: '',
|
||||
assigneeUsernamesQuery: '',
|
||||
issuesIncidentDetails: false,
|
||||
},
|
||||
stubs: {
|
||||
GlButton: true,
|
||||
|
@ -194,22 +193,7 @@ describe('Incidents List', () => {
|
|||
expect(findSeverity().length).toBe(mockIncidents.length);
|
||||
});
|
||||
|
||||
it('contains a link to the issue details page', () => {
|
||||
findTableRows()
|
||||
.at(0)
|
||||
.trigger('click');
|
||||
expect(visitUrl).toHaveBeenCalledWith(joinPaths(`/project/issues/`, mockIncidents[0].iid));
|
||||
});
|
||||
|
||||
it('contains a link to the incident details page', async () => {
|
||||
beforeEach(() =>
|
||||
mountComponent({
|
||||
data: { incidents: { list: mockIncidents }, incidentsCount: {} },
|
||||
loading: false,
|
||||
provide: { glFeatures: { issuesIncidentDetails: true } },
|
||||
}),
|
||||
);
|
||||
|
||||
findTableRows()
|
||||
.at(0)
|
||||
.trigger('click');
|
||||
|
|
|
@ -363,6 +363,24 @@ RSpec.describe AlertManagement::Alert do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.open_status?' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:status, :is_open_status) do
|
||||
:triggered | true
|
||||
:acknowledged | true
|
||||
:resolved | false
|
||||
:ignored | false
|
||||
nil | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'returns true when the status is open status' do
|
||||
expect(described_class.open_status?(status)).to eq(is_open_status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_reference' do
|
||||
it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") }
|
||||
end
|
||||
|
|
56
spec/services/web_hooks/destroy_service_spec.rb
Normal file
56
spec/services/web_hooks/destroy_service_spec.rb
Normal file
|
@ -0,0 +1,56 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WebHooks::DestroyService do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
subject { described_class.new(user) }
|
||||
|
||||
shared_examples 'batched destroys' do
|
||||
it 'destroys all hooks in batches' do
|
||||
stub_const("#{described_class}::BATCH_SIZE", 1)
|
||||
expect(subject).to receive(:delete_web_hook_logs_in_batches).exactly(4).times.and_call_original
|
||||
|
||||
expect do
|
||||
status = subject.execute(hook)
|
||||
expect(status[:async]).to be false
|
||||
end
|
||||
.to change { WebHook.count }.from(1).to(0)
|
||||
.and change { WebHookLog.count }.from(3).to(0)
|
||||
end
|
||||
|
||||
it 'returns an error if sync destroy fails' do
|
||||
expect(hook).to receive(:destroy).and_return(false)
|
||||
|
||||
result = subject.sync_destroy(hook)
|
||||
|
||||
expect(result[:status]).to eq(:error)
|
||||
expect(result[:message]).to eq("Unable to destroy #{hook.model_name.human}")
|
||||
end
|
||||
|
||||
it 'schedules an async delete' do
|
||||
stub_const('WebHooks::DestroyService::LOG_COUNT_THRESHOLD', 1)
|
||||
|
||||
expect(WebHooks::DestroyWorker).to receive(:perform_async).with(user.id, hook.id).and_call_original
|
||||
|
||||
status = subject.execute(hook)
|
||||
|
||||
expect(status[:async]).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'with system hook' do
|
||||
let_it_be(:hook) { create(:system_hook, url: "http://example.com") }
|
||||
let_it_be(:log) { create_list(:web_hook_log, 3, web_hook: hook) }
|
||||
|
||||
it_behaves_like 'batched destroys'
|
||||
end
|
||||
|
||||
context 'with project hook' do
|
||||
let_it_be(:hook) { create(:project_hook) }
|
||||
let_it_be(:log) { create_list(:web_hook_log, 3, web_hook: hook) }
|
||||
|
||||
it_behaves_like 'batched destroys'
|
||||
end
|
||||
end
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
RSpec.shared_examples 'project cache control headers' do
|
||||
before do
|
||||
project.update(visibility_level: visibility_level)
|
||||
project.update!(visibility_level: visibility_level)
|
||||
end
|
||||
|
||||
context 'when project is public' do
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'Web hook destroyer' do
|
||||
it 'displays a message about synchronous delete', :aggregate_failures do
|
||||
expect_next_instance_of(WebHooks::DestroyService) do |instance|
|
||||
expect(instance).to receive(:execute).with(anything).and_call_original
|
||||
end
|
||||
|
||||
delete :destroy, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
expect(flash[:notice]).to eq("#{hook.model_name.human} was deleted")
|
||||
end
|
||||
|
||||
it 'displays a message about async delete', :aggregate_failures do
|
||||
expect_next_instance_of(WebHooks::DestroyService) do |instance|
|
||||
expect(instance).to receive(:execute).with(anything).and_return({ status: :success, async: true } )
|
||||
end
|
||||
|
||||
delete :destroy, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
expect(flash[:notice]).to eq("#{hook.model_name.human} was scheduled for deletion")
|
||||
end
|
||||
|
||||
it 'displays an error if deletion failed', :aggregate_failures do
|
||||
expect_next_instance_of(WebHooks::DestroyService) do |instance|
|
||||
expect(instance).to receive(:execute).with(anything).and_return({ status: :error, async: true, message: "failed" } )
|
||||
end
|
||||
|
||||
delete :destroy, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
expect(flash[:alert]).to eq("failed")
|
||||
end
|
||||
end
|
|
@ -262,7 +262,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
context "when the namespace is owned by the GitLab user" do
|
||||
before do
|
||||
user.username = other_username
|
||||
user.save
|
||||
user.save!
|
||||
end
|
||||
|
||||
it "takes the existing namespace" do
|
||||
|
|
|
@ -44,7 +44,7 @@ RSpec.shared_examples 'authenticates sessionless user' do |path, format, params|
|
|||
.to increment(:user_unauthenticated_counter)
|
||||
end
|
||||
|
||||
personal_access_token.update(scopes: [:read_user])
|
||||
personal_access_token.update!(scopes: [:read_user])
|
||||
|
||||
get path, params: default_params.merge(private_token: personal_access_token.token)
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ RSpec.shared_examples 'an editable merge request' do
|
|||
end
|
||||
|
||||
it 'warns about version conflict' do
|
||||
merge_request.update(title: "New title")
|
||||
merge_request.update!(title: "New title")
|
||||
|
||||
fill_in 'merge_request_title', with: 'bug 345'
|
||||
fill_in 'merge_request_description', with: 'bug description'
|
||||
|
|
|
@ -227,7 +227,7 @@ RSpec.shared_examples 'common trace features' do
|
|||
let(:token) { 'my_secret_token' }
|
||||
|
||||
before do
|
||||
build.project.update(runners_token: token)
|
||||
build.project.update!(runners_token: token)
|
||||
trace.append(token, 0)
|
||||
end
|
||||
|
||||
|
@ -240,7 +240,7 @@ RSpec.shared_examples 'common trace features' do
|
|||
let(:token) { 'my_secret_token' }
|
||||
|
||||
before do
|
||||
build.update(token: token)
|
||||
build.update!(token: token)
|
||||
trace.append(token, 0)
|
||||
end
|
||||
|
||||
|
@ -531,7 +531,7 @@ RSpec.shared_examples 'trace with disabled live trace feature' do
|
|||
context "when erase old trace with 'save'" do
|
||||
before do
|
||||
build.send(:write_attribute, :trace, nil)
|
||||
build.save
|
||||
build.save # rubocop:disable Rails/SaveBang
|
||||
end
|
||||
|
||||
it 'old trace is not deleted' do
|
||||
|
|
|
@ -109,7 +109,7 @@ RSpec.shared_examples 'issuable quick actions' do
|
|||
QuickAction.new(
|
||||
action_text: "/unlock",
|
||||
before_action: -> {
|
||||
issuable.update(discussion_locked: true)
|
||||
issuable.update!(discussion_locked: true)
|
||||
},
|
||||
expectation: ->(noteable, can_use_quick_action) {
|
||||
if can_use_quick_action
|
||||
|
@ -128,7 +128,7 @@ RSpec.shared_examples 'issuable quick actions' do
|
|||
QuickAction.new(
|
||||
action_text: "/remove_milestone",
|
||||
before_action: -> {
|
||||
issuable.update(milestone_id: milestone.id)
|
||||
issuable.update!(milestone_id: milestone.id)
|
||||
},
|
||||
expectation: ->(noteable, can_use_quick_action) {
|
||||
if can_use_quick_action
|
||||
|
@ -171,7 +171,7 @@ RSpec.shared_examples 'issuable quick actions' do
|
|||
QuickAction.new(
|
||||
action_text: "/remove_estimate",
|
||||
before_action: -> {
|
||||
issuable.update(time_estimate: 30000)
|
||||
issuable.update!(time_estimate: 30000)
|
||||
},
|
||||
expectation: ->(noteable, can_use_quick_action) {
|
||||
if can_use_quick_action
|
||||
|
@ -228,7 +228,7 @@ RSpec.shared_examples 'issuable quick actions' do
|
|||
|
||||
before do
|
||||
project.add_developer(old_assignee)
|
||||
issuable.update(assignees: [old_assignee])
|
||||
issuable.update!(assignees: [old_assignee])
|
||||
end
|
||||
|
||||
context 'when user can update issuable' do
|
||||
|
|
|
@ -52,7 +52,7 @@ RSpec.shared_examples 'merge quick action' do
|
|||
context 'when the head diff changes in the meanwhile' do
|
||||
before do
|
||||
merge_request.source_branch = 'another_branch'
|
||||
merge_request.save
|
||||
merge_request.save!
|
||||
sign_in(user)
|
||||
visit project_merge_request_path(project, merge_request)
|
||||
end
|
||||
|
|
|
@ -24,7 +24,7 @@ RSpec.shared_examples 'note entity' do
|
|||
|
||||
context 'when note was edited' do
|
||||
before do
|
||||
note.update(updated_at: 1.minute.from_now, updated_by: user)
|
||||
note.update!(updated_at: 1.minute.from_now, updated_by: user)
|
||||
end
|
||||
|
||||
it 'exposes last_edited_at and last_edited_by elements' do
|
||||
|
@ -34,7 +34,7 @@ RSpec.shared_examples 'note entity' do
|
|||
|
||||
context 'when note is a system note' do
|
||||
before do
|
||||
note.update(system: true)
|
||||
note.update!(system: true)
|
||||
end
|
||||
|
||||
it 'exposes system_note_icon_name element' do
|
||||
|
|
59
spec/workers/web_hooks/destroy_worker_spec.rb
Normal file
59
spec/workers/web_hooks/destroy_worker_spec.rb
Normal file
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WebHooks::DestroyWorker do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
before_all do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
subject { described_class.new }
|
||||
|
||||
describe "#perform" do
|
||||
context 'with a Web hook' do
|
||||
let!(:hook) { create(:project_hook, project: project) }
|
||||
let!(:other_hook) { create(:project_hook, project: project) }
|
||||
let!(:log) { create(:web_hook_log, web_hook: hook) }
|
||||
let!(:other_log) { create(:web_hook_log, web_hook: other_hook) }
|
||||
|
||||
it "deletes the Web hook and logs", :aggregate_failures do
|
||||
expect { subject.perform(user.id, hook.id) }
|
||||
.to change { WebHookLog.count }.from(2).to(1)
|
||||
.and change { WebHook.count }.from(2).to(1)
|
||||
|
||||
expect(WebHook.find(other_hook.id)).to be_present
|
||||
expect(WebHookLog.find(other_log.id)).to be_present
|
||||
end
|
||||
|
||||
it "raises and tracks an error if destroy failed" do
|
||||
allow_next_instance_of(::WebHooks::DestroyService) do |instance|
|
||||
expect(instance).to receive(:sync_destroy).with(anything).and_return({ status: :error, message: "failed" })
|
||||
end
|
||||
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception)
|
||||
.with(an_instance_of(::WebHooks::DestroyService::DestroyError), web_hook_id: hook.id)
|
||||
.and_call_original
|
||||
expect { subject.perform(user.id, hook.id) }.to raise_error(::WebHooks::DestroyService::DestroyError)
|
||||
end
|
||||
|
||||
context 'with unknown hook' do
|
||||
it 'does not raise an error' do
|
||||
expect { subject.perform(user.id, non_existing_record_id) }.not_to raise_error
|
||||
|
||||
expect(WebHook.count).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unknown user' do
|
||||
it 'does not raise an error' do
|
||||
expect { subject.perform(non_existing_record_id, hook.id) }.not_to raise_error
|
||||
|
||||
expect(WebHook.count).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue