Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f44bf01f69
commit
0a9efe0288
63 changed files with 1353 additions and 261 deletions
|
@ -1,10 +1,17 @@
|
|||
#import "../fragments/alert_note.fragment.graphql"
|
||||
|
||||
mutation updateAlertStatus($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) {
|
||||
updateAlertStatus(input: { iid: $iid, status: $status, projectPath: $projectPath }) {
|
||||
errors
|
||||
alert {
|
||||
iid,
|
||||
status,
|
||||
endedAt
|
||||
endedAt,
|
||||
notes {
|
||||
nodes {
|
||||
...AlertNote
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,11 +50,11 @@ export default {
|
|||
<template>
|
||||
<!-- 200ms delay so not every mouseover triggers Popover -->
|
||||
<gl-popover :target="target" :delay="200" boundary="viewport" triggers="hover" placement="top">
|
||||
<div class="user-popover d-flex">
|
||||
<div class="p-1 flex-shrink-1">
|
||||
<user-avatar-image :img-src="user.avatarUrl" :size="60" css-classes="mr-2" />
|
||||
<div class="gl-p-3 gl-line-height-normal gl-display-flex" data-testid="user-popover">
|
||||
<div class="gl-p-2 flex-shrink-1">
|
||||
<user-avatar-image :img-src="user.avatarUrl" :size="60" css-classes="gl-mr-3!" />
|
||||
</div>
|
||||
<div class="p-1 w-100">
|
||||
<div class="gl-p-2 gl-w-full">
|
||||
<template v-if="userIsLoading">
|
||||
<!-- `gl-skeleton-loading` does not support equal length lines -->
|
||||
<!-- This can be migrated to `gl-skeleton-loader` when https://gitlab.com/gitlab-org/gitlab-ui/-/issues/872 is completed -->
|
||||
|
@ -62,33 +62,31 @@ export default {
|
|||
v-for="n in $options.maxSkeletonLines"
|
||||
:key="n"
|
||||
:lines="1"
|
||||
class="animation-container-small mb-1"
|
||||
class="animation-container-small gl-mb-2"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="mb-2">
|
||||
<h5 class="m-0">
|
||||
<div class="gl-mb-3">
|
||||
<h5 class="gl-m-0">
|
||||
{{ user.name }}
|
||||
</h5>
|
||||
<span class="text-secondary">@{{ user.username }}</span>
|
||||
<span class="gl-text-gray-700">@{{ user.username }}</span>
|
||||
</div>
|
||||
<div class="text-secondary">
|
||||
<div v-if="user.bio" class="d-flex mb-1">
|
||||
<icon name="profile" class="category-icon flex-shrink-0" />
|
||||
<span ref="bio" class="ml-1">{{ user.bio }}</span>
|
||||
<div class="gl-text-gray-700">
|
||||
<div v-if="user.bio" class="gl-display-flex gl-mb-2">
|
||||
<icon name="profile" class="gl-text-gray-600 gl-flex-shrink-0" />
|
||||
<span ref="bio" class="gl-ml-2">{{ user.bio }}</span>
|
||||
</div>
|
||||
<div v-if="user.workInformation" class="d-flex mb-1">
|
||||
<icon name="work" class="category-icon flex-shrink-0" />
|
||||
<span ref="workInformation" class="ml-1">{{ user.workInformation }}</span>
|
||||
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
|
||||
<icon name="work" class="gl-text-gray-600 gl-flex-shrink-0" />
|
||||
<span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="js-location text-secondary d-flex">
|
||||
<div v-if="user.location">
|
||||
<icon name="location" class="category-icon flex-shrink-0" />
|
||||
<span class="ml-1">{{ user.location }}</span>
|
||||
</div>
|
||||
<div v-if="user.location" class="js-location gl-text-gray-700 gl-display-flex">
|
||||
<icon name="location" class="gl-text-gray-600 flex-shrink-0" />
|
||||
<span class="gl-ml-2">{{ user.location }}</span>
|
||||
</div>
|
||||
<div v-if="statusHtml" class="js-user-status mt-2">
|
||||
<div v-if="statusHtml" class="js-user-status gl-mt-3">
|
||||
<span v-html="statusHtml"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -100,18 +100,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* user_popover component
|
||||
*/
|
||||
.user-popover {
|
||||
padding: $gl-padding-8;
|
||||
line-height: $gl-line-height;
|
||||
|
||||
.category-icon {
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
.suggest-gitlab-ci-yml {
|
||||
margin-top: -1em;
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ module Mutations
|
|||
private
|
||||
|
||||
def update_status(alert, status)
|
||||
::AlertManagement::UpdateAlertStatusService
|
||||
.new(alert, current_user, status)
|
||||
::AlertManagement::Alerts::UpdateService
|
||||
.new(alert, current_user, status: status)
|
||||
.execute
|
||||
end
|
||||
|
||||
|
|
|
@ -31,7 +31,8 @@ module SystemNoteHelper
|
|||
'designs_added' => 'doc-image',
|
||||
'designs_modified' => 'doc-image',
|
||||
'designs_removed' => 'doc-image',
|
||||
'designs_discussion_added' => 'doc-image'
|
||||
'designs_discussion_added' => 'doc-image',
|
||||
'status' => 'status'
|
||||
}.freeze
|
||||
|
||||
def system_note_icon_name(note)
|
||||
|
|
|
@ -19,6 +19,7 @@ class SystemNoteMetadata < ApplicationRecord
|
|||
title time_tracking branch milestone discussion task moved
|
||||
opened closed merged duplicate locked unlocked outdated
|
||||
tag due_date pinned_embed cherry_pick health_status approved unapproved
|
||||
status
|
||||
].freeze
|
||||
|
||||
validates :note, presence: true
|
||||
|
|
|
@ -12,19 +12,20 @@ module AlertManagement
|
|||
@alert = alert
|
||||
@current_user = current_user
|
||||
@params = params
|
||||
@param_errors = []
|
||||
end
|
||||
|
||||
def execute
|
||||
return error_no_permissions unless allowed?
|
||||
return error_no_updates if params.empty?
|
||||
|
||||
filter_assignees
|
||||
return error_no_assignee_permissions if unauthorized_assignees?
|
||||
filter_params
|
||||
return error_invalid_params if param_errors.any?
|
||||
|
||||
# Save old assignees for system notes
|
||||
old_assignees = alert.assignees.to_a
|
||||
|
||||
if alert.update(params)
|
||||
process_assignement(old_assignees)
|
||||
handle_changes(old_assignees: old_assignees)
|
||||
|
||||
success
|
||||
else
|
||||
|
@ -34,7 +35,8 @@ module AlertManagement
|
|||
|
||||
private
|
||||
|
||||
attr_reader :alert, :current_user, :params
|
||||
attr_reader :alert, :current_user, :params, :param_errors
|
||||
delegate :resolved?, to: :alert
|
||||
|
||||
def allowed?
|
||||
current_user&.can?(:update_alert_management_alert, alert)
|
||||
|
@ -58,40 +60,80 @@ module AlertManagement
|
|||
error(_('You have no permissions'))
|
||||
end
|
||||
|
||||
def error_no_updates
|
||||
error(_('Please provide attributes to update'))
|
||||
def error_invalid_params
|
||||
error(param_errors.to_sentence)
|
||||
end
|
||||
|
||||
def error_no_assignee_permissions
|
||||
error(_('Assignee has no permissions'))
|
||||
def add_param_error(message)
|
||||
param_errors << message
|
||||
end
|
||||
|
||||
def filter_params
|
||||
param_errors << _('Please provide attributes to update') if params.empty?
|
||||
|
||||
filter_status
|
||||
filter_assignees
|
||||
end
|
||||
|
||||
def handle_changes(old_assignees:)
|
||||
handle_assignement(old_assignees) if params[:assignees]
|
||||
handle_status_change if params[:status_event]
|
||||
end
|
||||
|
||||
# ----- Assignee-related behavior ------
|
||||
def unauthorized_assignees?
|
||||
params[:assignees]&.any? { |user| !user.can?(:read_alert_management_alert, alert) }
|
||||
end
|
||||
|
||||
def filter_assignees
|
||||
return if params[:assignees].nil?
|
||||
|
||||
# Always take first assignee while multiple are not currently supported
|
||||
params[:assignees] = Array(params[:assignees].first)
|
||||
|
||||
param_errors << _('Assignee has no permissions') if unauthorized_assignees?
|
||||
end
|
||||
|
||||
def process_assignement(old_assignees)
|
||||
def unauthorized_assignees?
|
||||
params[:assignees]&.any? { |user| !user.can?(:read_alert_management_alert, alert) }
|
||||
end
|
||||
|
||||
def handle_assignement(old_assignees)
|
||||
assign_todo
|
||||
add_assignee_system_note(old_assignees)
|
||||
end
|
||||
|
||||
def assign_todo
|
||||
return if alert.assignees.empty?
|
||||
|
||||
todo_service.assign_alert(alert, current_user)
|
||||
end
|
||||
|
||||
def add_assignee_system_note(old_assignees)
|
||||
SystemNoteService.change_issuable_assignees(alert, alert.project, current_user, old_assignees)
|
||||
end
|
||||
|
||||
# ------ Status-related behavior -------
|
||||
def filter_status
|
||||
return unless status = params.delete(:status)
|
||||
|
||||
status_key = AlertManagement::Alert::STATUSES.key(status)
|
||||
status_event = AlertManagement::Alert::STATUS_EVENTS[status_key]
|
||||
|
||||
unless status_event
|
||||
param_errors << _('Invalid status')
|
||||
return
|
||||
end
|
||||
|
||||
params[:status_event] = status_event
|
||||
end
|
||||
|
||||
def handle_status_change
|
||||
add_status_change_system_note
|
||||
resolve_todos if resolved?
|
||||
end
|
||||
|
||||
def add_status_change_system_note
|
||||
SystemNoteService.change_alert_status(alert, current_user)
|
||||
end
|
||||
|
||||
def resolve_todos
|
||||
todo_service.resolve_todos_for_target(alert, current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AlertManagement
|
||||
class UpdateAlertStatusService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
# @param alert [AlertManagement::Alert]
|
||||
# @param user [User]
|
||||
# @param status [Integer] Must match a value from AlertManagement::Alert::STATUSES
|
||||
def initialize(alert, user, status)
|
||||
@alert = alert
|
||||
@user = user
|
||||
@status = status
|
||||
end
|
||||
|
||||
def execute
|
||||
return error_no_permissions unless allowed?
|
||||
return error_invalid_status unless status_key
|
||||
|
||||
if alert.update(status_event: status_event)
|
||||
resolve_todos if resolved?
|
||||
success
|
||||
else
|
||||
error(alert.errors.full_messages.to_sentence)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :alert, :user, :status
|
||||
|
||||
delegate :project, :resolved?, to: :alert
|
||||
|
||||
def allowed?
|
||||
user.can?(:update_alert_management_alert, project)
|
||||
end
|
||||
|
||||
def resolve_todos
|
||||
TodoService.new.resolve_todos_for_target(alert, user)
|
||||
end
|
||||
|
||||
def status_key
|
||||
strong_memoize(:status_key) do
|
||||
AlertManagement::Alert::STATUSES.key(status)
|
||||
end
|
||||
end
|
||||
|
||||
def status_event
|
||||
AlertManagement::Alert::STATUS_EVENTS[status_key]
|
||||
end
|
||||
|
||||
def success
|
||||
ServiceResponse.success(payload: { alert: alert })
|
||||
end
|
||||
|
||||
def error_no_permissions
|
||||
error(_('You have no permissions'))
|
||||
end
|
||||
|
||||
def error_invalid_status
|
||||
error(_('Invalid status'))
|
||||
end
|
||||
|
||||
def error(message)
|
||||
ServiceResponse.error(payload: { alert: alert }, message: message)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -292,6 +292,10 @@ module SystemNoteService
|
|||
merge_requests_service(noteable, noteable.project, user).unapprove_mr
|
||||
end
|
||||
|
||||
def change_alert_status(alert, author)
|
||||
::SystemNotes::AlertManagementService.new(noteable: alert, project: alert.project, author: author).change_alert_status(alert)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def merge_requests_service(noteable, project, author)
|
||||
|
|
21
app/services/system_notes/alert_management_service.rb
Normal file
21
app/services/system_notes/alert_management_service.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SystemNotes
|
||||
class AlertManagementService < ::SystemNotes::BaseService
|
||||
# Called when the status of an AlertManagement::Alert has changed
|
||||
#
|
||||
# alert - AlertManagement::Alert object.
|
||||
#
|
||||
# Example Note text:
|
||||
#
|
||||
# "changed the status to Acknowledged"
|
||||
#
|
||||
# Returns the created Note object
|
||||
def change_alert_status(alert)
|
||||
status = AlertManagement::Alert::STATUSES.key(alert.status).to_s.titleize
|
||||
body = "changed the status to **#{status}**"
|
||||
|
||||
create_note(NoteSummary.new(noteable, project, author, body, action: 'status'))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move verify stage usage activity to CE
|
||||
merge_request: 36090
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Deduplicate labels with identical title and project
|
||||
merge_request: 21384
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/nfriend-fix-npm-template.yml
Normal file
5
changelogs/unreleased/nfriend-fix-npm-template.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove hardcoded reference to gitlab.com in NPM .gitlab-ci.yml template
|
||||
merge_request: 36124
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/sy-alert-status-system-notes.yml
Normal file
5
changelogs/unreleased/sy-alert-status-system-notes.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add system notes for status updates on alerts
|
||||
merge_request: 35467
|
||||
author:
|
||||
type: added
|
31
db/migrate/20200305020458_add_label_restore_table.rb
Normal file
31
db/migrate/20200305020458_add_label_restore_table.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddLabelRestoreTable < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
# copy table
|
||||
execute "CREATE TABLE #{backup_labels_table_name} (LIKE #{labels_table_name} INCLUDING ALL);"
|
||||
|
||||
# make the primary key a real functioning one rather than incremental
|
||||
execute "ALTER TABLE #{backup_labels_table_name} ALTER COLUMN ID DROP DEFAULT;"
|
||||
|
||||
# add some fields that make changes trackable
|
||||
execute "ALTER TABLE #{backup_labels_table_name} ADD COLUMN restore_action INTEGER;"
|
||||
execute "ALTER TABLE #{backup_labels_table_name} ADD COLUMN new_title VARCHAR;"
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table backup_labels_table_name
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def labels_table_name
|
||||
:labels
|
||||
end
|
||||
|
||||
def backup_labels_table_name
|
||||
:backup_labels
|
||||
end
|
||||
end
|
35
db/migrate/20200305020459_add_label_restore_foreign_keys.rb
Normal file
35
db/migrate/20200305020459_add_label_restore_foreign_keys.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddLabelRestoreForeignKeys < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
# create foreign keys
|
||||
connection.foreign_keys(labels_table_name).each do |fk|
|
||||
fk_options = fk.options
|
||||
add_concurrent_foreign_key(backup_labels_table_name, fk.to_table, name: fk.name, column: fk_options[:column])
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
connection.foreign_keys(backup_labels_table_name).each do |fk|
|
||||
with_lock_retries do
|
||||
remove_foreign_key backup_labels_table_name, name: fk.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def labels_table_name
|
||||
:labels
|
||||
end
|
||||
|
||||
def backup_labels_table_name
|
||||
:backup_labels
|
||||
end
|
||||
end
|
|
@ -0,0 +1,130 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveDuplicateLabelsFromProject < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
CREATE = 1
|
||||
RENAME = 2
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
class BackupLabel < Label
|
||||
self.table_name = 'backup_labels'
|
||||
end
|
||||
|
||||
class Label < ApplicationRecord
|
||||
self.table_name = 'labels'
|
||||
end
|
||||
|
||||
class Project < ApplicationRecord
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'projects'
|
||||
end
|
||||
|
||||
BATCH_SIZE = 100_000
|
||||
|
||||
def up
|
||||
# Split to smaller chunks
|
||||
# Loop rather than background job, every 100,000
|
||||
# there are 45,000,000 projects in total
|
||||
Project.each_batch(of: BATCH_SIZE) do |batch|
|
||||
range = batch.pluck('MIN(id)', 'MAX(id)').first
|
||||
|
||||
transaction do
|
||||
remove_full_duplicates(*range)
|
||||
end
|
||||
|
||||
transaction do
|
||||
rename_partial_duplicates(*range)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
Project.each_batch(of: BATCH_SIZE) do |batch|
|
||||
range = batch.pluck('MIN(id)', 'MAX(id)').first
|
||||
|
||||
restore_renamed_labels(*range)
|
||||
restore_deleted_labels(*range)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_full_duplicates(start_id, stop_id)
|
||||
# Fields that are considered duplicate:
|
||||
# project_id title template description type color
|
||||
|
||||
duplicate_labels = ApplicationRecord.connection.execute(<<-SQL.squish)
|
||||
WITH data AS (
|
||||
SELECT labels.*,
|
||||
row_number() OVER (PARTITION BY labels.project_id, labels.title, labels.template, labels.description, labels.type, labels.color ORDER BY labels.id) AS row_number,
|
||||
#{CREATE} AS restore_action
|
||||
FROM labels
|
||||
WHERE labels.project_id BETWEEN #{start_id} AND #{stop_id}
|
||||
AND NOT EXISTS (SELECT * FROM board_labels WHERE board_labels.label_id = labels.id)
|
||||
AND NOT EXISTS (SELECT * FROM label_links WHERE label_links.label_id = labels.id)
|
||||
AND NOT EXISTS (SELECT * FROM label_priorities WHERE label_priorities.label_id = labels.id)
|
||||
AND NOT EXISTS (SELECT * FROM lists WHERE lists.label_id = labels.id)
|
||||
AND NOT EXISTS (SELECT * FROM resource_label_events WHERE resource_label_events.label_id = labels.id)
|
||||
) SELECT * FROM data WHERE row_number > 1;
|
||||
SQL
|
||||
|
||||
if duplicate_labels.any?
|
||||
# create backup records
|
||||
BackupLabel.insert_all!(duplicate_labels.map { |label| label.except("row_number") })
|
||||
|
||||
Label.where(id: duplicate_labels.pluck("id")).delete_all
|
||||
end
|
||||
end
|
||||
|
||||
def rename_partial_duplicates(start_id, stop_id)
|
||||
# We need to ensure that the new title (with `_duplicate#{ID}`) doesn't exceed the limit.
|
||||
# Truncate the original title (if needed) to 245 characters minus the length of the ID
|
||||
# then add `_duplicate#{ID}`
|
||||
|
||||
soft_duplicates = ApplicationRecord.connection.execute(<<-SQL.squish)
|
||||
WITH data AS (
|
||||
SELECT
|
||||
*,
|
||||
substring(title from 1 for 245 - length(id::text)) || '_duplicate' || id::text as new_title,
|
||||
#{RENAME} AS restore_action,
|
||||
row_number() OVER (PARTITION BY project_id, title ORDER BY id) AS row_number
|
||||
FROM labels
|
||||
WHERE project_id BETWEEN #{start_id} AND #{stop_id}
|
||||
) SELECT * FROM data WHERE row_number > 1;
|
||||
SQL
|
||||
|
||||
if soft_duplicates.any?
|
||||
# create backup records
|
||||
BackupLabel.insert_all!(soft_duplicates.map { |label| label.except("row_number") })
|
||||
|
||||
ApplicationRecord.connection.execute(<<-SQL.squish)
|
||||
UPDATE labels SET title = substring(title from 1 for 245 - length(id::text)) || '_duplicate' || id::text
|
||||
WHERE labels.id IN (#{soft_duplicates.map { |dup| dup["id"] }.join(", ")});
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def restore_renamed_labels(start_id, stop_id)
|
||||
# the backup label IDs are not incremental, they are copied directly from the Labels table
|
||||
ApplicationRecord.connection.execute(<<-SQL.squish)
|
||||
WITH backups AS (
|
||||
SELECT id, title
|
||||
FROM backup_labels
|
||||
WHERE project_id BETWEEN #{start_id} AND #{stop_id} AND
|
||||
restore_action = #{RENAME}
|
||||
) UPDATE labels SET title = backups.title
|
||||
FROM backups
|
||||
WHERE labels.id = backups.id;
|
||||
SQL
|
||||
end
|
||||
|
||||
def restore_deleted_labels(start_id, stop_id)
|
||||
ActiveRecord::Base.connection.execute(<<-SQL.squish)
|
||||
INSERT INTO labels
|
||||
SELECT id, title, color, project_id, created_at, updated_at, template, description, description_html, type, group_id, cached_markdown_version FROM backup_labels
|
||||
WHERE backup_labels.project_id BETWEEN #{start_id} AND #{stop_id}
|
||||
AND backup_labels.restore_action = #{CREATE}
|
||||
SQL
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddUniquenessIndexToLabelTitleAndProject < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
PROJECT_AND_TITLE = [:project_id, :title]
|
||||
|
||||
def up
|
||||
add_concurrent_index :labels, PROJECT_AND_TITLE, where: "labels.group_id IS NULL", unique: true, name: "index_labels_on_project_id_and_title_unique"
|
||||
remove_concurrent_index :labels, PROJECT_AND_TITLE, name: "index_labels_on_project_id_and_title"
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :labels, PROJECT_AND_TITLE, where: "labels.group_id IS NULL", unique: false, name: "index_labels_on_project_id_and_title"
|
||||
remove_concurrent_index :labels, PROJECT_AND_TITLE, name: "index_labels_on_project_id_and_title_unique"
|
||||
end
|
||||
end
|
|
@ -9413,6 +9413,23 @@ CREATE TABLE public.aws_roles (
|
|||
role_external_id character varying(64) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE public.backup_labels (
|
||||
id integer NOT NULL,
|
||||
title character varying,
|
||||
color character varying,
|
||||
project_id integer,
|
||||
created_at timestamp without time zone,
|
||||
updated_at timestamp without time zone,
|
||||
template boolean DEFAULT false,
|
||||
description character varying,
|
||||
description_html text,
|
||||
type character varying,
|
||||
group_id integer,
|
||||
cached_markdown_version integer,
|
||||
restore_action integer,
|
||||
new_title character varying
|
||||
);
|
||||
|
||||
CREATE TABLE public.badges (
|
||||
id integer NOT NULL,
|
||||
link_url character varying NOT NULL,
|
||||
|
@ -17234,6 +17251,9 @@ ALTER TABLE ONLY public.award_emoji
|
|||
ALTER TABLE ONLY public.aws_roles
|
||||
ADD CONSTRAINT aws_roles_pkey PRIMARY KEY (user_id);
|
||||
|
||||
ALTER TABLE ONLY public.backup_labels
|
||||
ADD CONSTRAINT backup_labels_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.badges
|
||||
ADD CONSTRAINT badges_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -18368,6 +18388,20 @@ CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON public.approv
|
|||
|
||||
CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE ((code_owner = true) AND (section IS NULL));
|
||||
|
||||
CREATE UNIQUE INDEX backup_labels_group_id_project_id_title_idx ON public.backup_labels USING btree (group_id, project_id, title);
|
||||
|
||||
CREATE INDEX backup_labels_group_id_title_idx ON public.backup_labels USING btree (group_id, title) WHERE (project_id = NULL::integer);
|
||||
|
||||
CREATE INDEX backup_labels_project_id_idx ON public.backup_labels USING btree (project_id);
|
||||
|
||||
CREATE UNIQUE INDEX backup_labels_project_id_title_idx ON public.backup_labels USING btree (project_id, title) WHERE (group_id = NULL::integer);
|
||||
|
||||
CREATE INDEX backup_labels_template_idx ON public.backup_labels USING btree (template) WHERE template;
|
||||
|
||||
CREATE INDEX backup_labels_title_idx ON public.backup_labels USING btree (title);
|
||||
|
||||
CREATE INDEX backup_labels_type_project_id_idx ON public.backup_labels USING btree (type, project_id);
|
||||
|
||||
CREATE INDEX ci_builds_gitlab_monitor_metrics ON public.ci_builds USING btree (status, created_at, project_id) WHERE ((type)::text = 'Ci::Build'::text);
|
||||
|
||||
CREATE INDEX code_owner_approval_required ON public.protected_branches USING btree (project_id, code_owner_approval_required) WHERE (code_owner_approval_required = true);
|
||||
|
@ -19374,7 +19408,7 @@ CREATE INDEX index_labels_on_group_id_and_title ON public.labels USING btree (gr
|
|||
|
||||
CREATE INDEX index_labels_on_project_id ON public.labels USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_labels_on_project_id_and_title ON public.labels USING btree (project_id, title) WHERE (group_id = NULL::integer);
|
||||
CREATE UNIQUE INDEX index_labels_on_project_id_and_title_unique ON public.labels USING btree (project_id, title) WHERE (group_id IS NULL);
|
||||
|
||||
CREATE INDEX index_labels_on_template ON public.labels USING btree (template) WHERE template;
|
||||
|
||||
|
@ -21016,6 +21050,9 @@ ALTER TABLE ONLY public.vulnerabilities
|
|||
ALTER TABLE ONLY public.labels
|
||||
ADD CONSTRAINT fk_7de4989a69 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.backup_labels
|
||||
ADD CONSTRAINT fk_7de4989a69 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.merge_requests
|
||||
ADD CONSTRAINT fk_7e85395a64 FOREIGN KEY (sprint_id) REFERENCES public.sprints(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -22222,6 +22259,9 @@ ALTER TABLE ONLY public.serverless_domain_cluster
|
|||
ALTER TABLE ONLY public.labels
|
||||
ADD CONSTRAINT fk_rails_c1ac5161d8 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.backup_labels
|
||||
ADD CONSTRAINT fk_rails_c1ac5161d8 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.project_feature_usages
|
||||
ADD CONSTRAINT fk_rails_c22a50024b FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -23208,6 +23248,10 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200304160801
|
||||
20200304160823
|
||||
20200304211738
|
||||
20200305020458
|
||||
20200305020459
|
||||
20200305082754
|
||||
20200305082858
|
||||
20200305121159
|
||||
20200305151736
|
||||
20200305200641
|
||||
|
|
|
@ -173,7 +173,6 @@ configuration option in `gitlab.yml`. These metrics are served from the
|
|||
| `geo_repositories_retrying_verification_count` | Gauge | 11.2 | Number of repositories verification failures that Geo is actively trying to correct on secondary | `url` |
|
||||
| `geo_wikis_retrying_verification_count` | Gauge | 11.2 | Number of wikis verification failures that Geo is actively trying to correct on secondary | `url` |
|
||||
| `global_search_bulk_cron_queue_size` | Gauge | 12.10 | Number of database records waiting to be synchronized to Elasticsearch | |
|
||||
| `global_search_awaiting_indexing_queue_size` | Gauge | 13.2 | Number of database updates waiting to be synchronized to Elasticsearch while indexing is paused | |
|
||||
| `package_files_count` | Gauge | 13.0 | Number of package files on primary | `url` |
|
||||
| `package_files_checksummed_count` | Gauge | 13.0 | Number of package files checksummed on primary | `url` |
|
||||
| `package_files_checksum_failed_count` | Gauge | 13.0 | Number of package files failed to calculate the checksum on primary
|
||||
|
|
|
@ -678,16 +678,16 @@ appear to be associated to any of the services running, since they all appear to
|
|||
| `releases` | `usage_activity_by_stage` | `release` | | CE+EE | Unique release tags in project |
|
||||
| `successful_deployments` | `usage_activity_by_stage` | `release` | | CE+EE | Total successful deployments |
|
||||
| `user_preferences_group_overview_security_dashboard: 0` | `usage_activity_by_stage` | `secure` | | | |
|
||||
| `ci_builds` | `usage_activity_by_stage` | `verify` | | | Unique builds in project |
|
||||
| `ci_external_pipelines` | `usage_activity_by_stage` | `verify` | | | Total pipelines in external repositories |
|
||||
| `ci_internal_pipelines` | `usage_activity_by_stage` | `verify` | | | Total pipelines in GitLab repositories |
|
||||
| `ci_pipeline_config_auto_devops` | `usage_activity_by_stage` | `verify` | | | Total pipelines from an Auto DevOps template |
|
||||
| `ci_pipeline_config_repository` | `usage_activity_by_stage` | `verify` | | | Pipelines from templates in repository |
|
||||
| `ci_pipeline_schedules` | `usage_activity_by_stage` | `verify` | | | Pipeline schedules in GitLab |
|
||||
| `ci_pipelines` | `usage_activity_by_stage` | `verify` | | | Total pipelines |
|
||||
| `ci_triggers` | `usage_activity_by_stage` | `verify` | | | Triggers enabled |
|
||||
| `clusters_applications_runner` | `usage_activity_by_stage` | `verify` | | | Unique clusters with Runner enabled |
|
||||
| `projects_reporting_ci_cd_back_to_github: 0` | `usage_activity_by_stage` | `verify` | | | Unique projects with a GitHub pipeline enabled |
|
||||
| `ci_builds` | `usage_activity_by_stage` | `verify` | | CE+EE | Unique builds in project |
|
||||
| `ci_external_pipelines` | `usage_activity_by_stage` | `verify` | | CE+EE | Total pipelines in external repositories |
|
||||
| `ci_internal_pipelines` | `usage_activity_by_stage` | `verify` | | CE+EE | Total pipelines in GitLab repositories |
|
||||
| `ci_pipeline_config_auto_devops` | `usage_activity_by_stage` | `verify` | | CE+EE | Total pipelines from an Auto DevOps template |
|
||||
| `ci_pipeline_config_repository` | `usage_activity_by_stage` | `verify` | | CE+EE | Pipelines from templates in repository |
|
||||
| `ci_pipeline_schedules` | `usage_activity_by_stage` | `verify` | | CE+EE | Pipeline schedules in GitLab |
|
||||
| `ci_pipelines` | `usage_activity_by_stage` | `verify` | | CE+EE | Total pipelines |
|
||||
| `ci_triggers` | `usage_activity_by_stage` | `verify` | | CE+EE | Triggers enabled |
|
||||
| `clusters_applications_runner` | `usage_activity_by_stage` | `verify` | | CE+EE | Unique clusters with Runner enabled |
|
||||
| `projects_reporting_ci_cd_back_to_github` | `usage_activity_by_stage` | `verify` | | EE | Unique projects with a GitHub pipeline enabled |
|
||||
| `merge_requests_users` | `usage_activity_by_stage_monthly` | `create` | | | Unique count of users who used a merge request |
|
||||
| `duration_s` | `topology` | `enablement` | | | Time it took to collect topology data |
|
||||
| `application_requests_per_hour` | `topology` | `enablement` | | | Number of requests to the web application per hour |
|
||||
|
|
|
@ -251,3 +251,16 @@ If you sort by `Priority`, GitLab uses this sort comparison order:
|
|||
Ties are broken arbitrarily.
|
||||
|
||||
![Labels sort priority](img/labels_sort_priority.png)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Some label titles end with `_duplicate<number>`
|
||||
|
||||
In specific circumstances it was possible to create labels with duplicate titles in the same
|
||||
namespace.
|
||||
|
||||
To resolve the duplication, [in GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21384)
|
||||
and later, some duplicate labels have `_duplicate<number>` appended to their titles.
|
||||
|
||||
You can safely change these labels' titles if you prefer.
|
||||
For details of the original problem, see [issue 30390](https://gitlab.com/gitlab-org/gitlab/issues/30390).
|
||||
|
|
11
lib/api/entities/conan_package/conan_package_manifest.rb
Normal file
11
lib/api/entities/conan_package/conan_package_manifest.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module ConanPackage
|
||||
class ConanPackageManifest < Grape::Entity
|
||||
expose :package_urls, merge: true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
lib/api/entities/conan_package/conan_package_snapshot.rb
Normal file
11
lib/api/entities/conan_package/conan_package_snapshot.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module ConanPackage
|
||||
class ConanPackageSnapshot < Grape::Entity
|
||||
expose :package_snapshot, merge: true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
lib/api/entities/conan_package/conan_recipe_manifest.rb
Normal file
11
lib/api/entities/conan_package/conan_recipe_manifest.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module ConanPackage
|
||||
class ConanRecipeManifest < Grape::Entity
|
||||
expose :recipe_urls, merge: true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
lib/api/entities/conan_package/conan_recipe_snapshot.rb
Normal file
11
lib/api/entities/conan_package/conan_recipe_snapshot.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module ConanPackage
|
||||
class ConanRecipeSnapshot < Grape::Entity
|
||||
expose :recipe_snapshot, merge: true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
lib/api/entities/conan_package/conan_upload_urls.rb
Normal file
11
lib/api/entities/conan_package/conan_upload_urls.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module ConanPackage
|
||||
class ConanUploadUrls < Grape::Entity
|
||||
expose :upload_urls, merge: true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
19
lib/api/entities/entity_helpers.rb
Normal file
19
lib/api/entities/entity_helpers.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module EntityHelpers
|
||||
def can_read(attr, &block)
|
||||
->(obj, opts) { Ability.allowed?(opts[:user], "read_#{attr}".to_sym, yield(obj)) }
|
||||
end
|
||||
|
||||
def can_destroy(attr, &block)
|
||||
->(obj, opts) { Ability.allowed?(opts[:user], "destroy_#{attr}".to_sym, yield(obj)) }
|
||||
end
|
||||
|
||||
def expose_restricted(attr, &block)
|
||||
expose attr, if: can_read(attr, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
lib/api/entities/npm_package.rb
Normal file
11
lib/api/entities/npm_package.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class NpmPackage < Grape::Entity
|
||||
expose :name
|
||||
expose :versions
|
||||
expose :dist_tags, as: 'dist-tags'
|
||||
end
|
||||
end
|
||||
end
|
9
lib/api/entities/npm_package_tag.rb
Normal file
9
lib/api/entities/npm_package_tag.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class NpmPackageTag < Grape::Entity
|
||||
expose :dist_tags, merge: true
|
||||
end
|
||||
end
|
||||
end
|
14
lib/api/entities/nuget/dependency.rb
Normal file
14
lib/api/entities/nuget/dependency.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Nuget
|
||||
class Dependency < Grape::Entity
|
||||
expose :id, as: :@id
|
||||
expose :type, as: :@type
|
||||
expose :name, as: :id
|
||||
expose :range
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
14
lib/api/entities/nuget/dependency_group.rb
Normal file
14
lib/api/entities/nuget/dependency_group.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Nuget
|
||||
class DependencyGroup < Grape::Entity
|
||||
expose :id, as: :@id
|
||||
expose :type, as: :@type
|
||||
expose :target_framework, as: :targetFramework, expose_nil: false
|
||||
expose :dependencies, using: ::API::Entities::Nuget::Dependency
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
13
lib/api/entities/nuget/metadatum.rb
Normal file
13
lib/api/entities/nuget/metadatum.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Nuget
|
||||
class Metadatum < Grape::Entity
|
||||
expose :project_url, as: :projectUrl, expose_nil: false
|
||||
expose :license_url, as: :licenseUrl, expose_nil: false
|
||||
expose :icon_url, as: :iconUrl, expose_nil: false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
13
lib/api/entities/nuget/package_metadata.rb
Normal file
13
lib/api/entities/nuget/package_metadata.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Nuget
|
||||
class PackageMetadata < Grape::Entity
|
||||
expose :json_url, as: :@id
|
||||
expose :archive_url, as: :packageContent
|
||||
expose :catalog_entry, as: :catalogEntry, using: ::API::Entities::Nuget::PackageMetadataCatalogEntry
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
19
lib/api/entities/nuget/package_metadata_catalog_entry.rb
Normal file
19
lib/api/entities/nuget/package_metadata_catalog_entry.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Nuget
|
||||
class PackageMetadataCatalogEntry < Grape::Entity
|
||||
expose :json_url, as: :@id
|
||||
expose :authors
|
||||
expose :dependency_groups, as: :dependencyGroups, using: ::API::Entities::Nuget::DependencyGroup
|
||||
expose :package_name, as: :id
|
||||
expose :package_version, as: :version
|
||||
expose :tags
|
||||
expose :archive_url, as: :packageContent
|
||||
expose :summary
|
||||
expose :metadatum, using: ::API::Entities::Nuget::Metadatum, merge: true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
12
lib/api/entities/nuget/packages_metadata.rb
Normal file
12
lib/api/entities/nuget/packages_metadata.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Nuget
|
||||
class PackagesMetadata < Grape::Entity
|
||||
expose :count
|
||||
expose :items, using: ::API::Entities::Nuget::PackagesMetadataItem
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
15
lib/api/entities/nuget/packages_metadata_item.rb
Normal file
15
lib/api/entities/nuget/packages_metadata_item.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Nuget
|
||||
class PackagesMetadataItem < Grape::Entity
|
||||
expose :json_url, as: :@id
|
||||
expose :lower_version, as: :lower
|
||||
expose :upper_version, as: :upper
|
||||
expose :packages_count, as: :count
|
||||
expose :packages, as: :items, using: ::API::Entities::Nuget::PackageMetadata
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
lib/api/entities/nuget/packages_versions.rb
Normal file
11
lib/api/entities/nuget/packages_versions.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Nuget
|
||||
class PackagesVersions < Grape::Entity
|
||||
expose :versions
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
21
lib/api/entities/nuget/search_result.rb
Normal file
21
lib/api/entities/nuget/search_result.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Nuget
|
||||
class SearchResult < Grape::Entity
|
||||
expose :type, as: :@type
|
||||
expose :authors
|
||||
expose :name, as: :id
|
||||
expose :name, as: :title
|
||||
expose :summary
|
||||
expose :total_downloads, as: :totalDownloads
|
||||
expose :verified
|
||||
expose :version
|
||||
expose :versions, using: ::API::Entities::Nuget::SearchResultVersion
|
||||
expose :tags
|
||||
expose :metadatum, using: ::API::Entities::Nuget::Metadatum, merge: true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
13
lib/api/entities/nuget/search_result_version.rb
Normal file
13
lib/api/entities/nuget/search_result_version.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Nuget
|
||||
class SearchResultVersion < Grape::Entity
|
||||
expose :json_url, as: :@id
|
||||
expose :version
|
||||
expose :downloads
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
12
lib/api/entities/nuget/search_results.rb
Normal file
12
lib/api/entities/nuget/search_results.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Nuget
|
||||
class SearchResults < Grape::Entity
|
||||
expose :total_count, as: :totalHits
|
||||
expose :data, using: ::API::Entities::Nuget::SearchResult
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
12
lib/api/entities/nuget/service_index.rb
Normal file
12
lib/api/entities/nuget/service_index.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Nuget
|
||||
class ServiceIndex < Grape::Entity
|
||||
expose :version
|
||||
expose :resources
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
40
lib/api/entities/package.rb
Normal file
40
lib/api/entities/package.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class Package < Grape::Entity
|
||||
include ::API::Helpers::RelatedResourcesHelpers
|
||||
extend ::API::Entities::EntityHelpers
|
||||
|
||||
expose :id
|
||||
expose :name
|
||||
expose :version
|
||||
expose :package_type
|
||||
|
||||
expose :_links do
|
||||
expose :web_path do |package|
|
||||
::Gitlab::Routing.url_helpers.project_package_path(package.project, package)
|
||||
end
|
||||
|
||||
expose :delete_api_path, if: can_destroy(:package, &:project) do |package|
|
||||
expose_url api_v4_projects_packages_path(package_id: package.id, id: package.project_id)
|
||||
end
|
||||
end
|
||||
|
||||
expose :created_at
|
||||
expose :project_id, if: ->(_, opts) { opts[:group] }
|
||||
expose :project_path, if: ->(obj, opts) { opts[:group] && Ability.allowed?(opts[:user], :read_project, obj.project) }
|
||||
expose :tags
|
||||
|
||||
expose :pipeline, if: ->(package) { package.build_info }, using: Package::Pipeline
|
||||
|
||||
expose :versions, using: ::API::Entities::PackageVersion
|
||||
|
||||
private
|
||||
|
||||
def project_path
|
||||
object.project.full_path
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
lib/api/entities/package/pipeline.rb
Normal file
11
lib/api/entities/package/pipeline.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class Package < Grape::Entity
|
||||
class Pipeline < ::API::Entities::PipelineBasic
|
||||
expose :user, using: ::API::Entities::UserBasic
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
lib/api/entities/package_file.rb
Normal file
11
lib/api/entities/package_file.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class PackageFile < Grape::Entity
|
||||
expose :id, :package_id, :created_at
|
||||
expose :file_name, :size
|
||||
expose :file_md5, :file_sha1
|
||||
end
|
||||
end
|
||||
end
|
14
lib/api/entities/package_version.rb
Normal file
14
lib/api/entities/package_version.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class PackageVersion < Grape::Entity
|
||||
expose :id
|
||||
expose :version
|
||||
expose :created_at
|
||||
expose :tags
|
||||
|
||||
expose :pipeline, if: ->(package) { package.build_info }, using: Package::Pipeline
|
||||
end
|
||||
end
|
||||
end
|
|
@ -34,9 +34,9 @@ create_npmrc:
|
|||
echo 'No .npmrc found! Creating one now. Please review the following link for more information: https://docs.gitlab.com/ee/user/packages/npm_registry/index.html#authenticating-with-a-ci-job-token'
|
||||
|
||||
{
|
||||
echo "@${CI_PROJECT_ROOT_NAMESPACE}:registry=\${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/"
|
||||
echo '//gitlab.com/api/v4/packages/npm/:_authToken=${CI_JOB_TOKEN}'
|
||||
echo '//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}'
|
||||
echo '@${CI_PROJECT_ROOT_NAMESPACE}:registry=${CI_SERVER_PROTOCOL}://${CI_SERVER_HOST}:${CI_SERVER_PORT}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/'
|
||||
echo '//${CI_SERVER_HOST}:${CI_SERVER_PORT}/api/v4/packages/npm/:_authToken=${CI_JOB_TOKEN}'
|
||||
echo '//${CI_SERVER_HOST}:${CI_SERVER_PORT}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}'
|
||||
} >> .npmrc
|
||||
|
||||
fi
|
||||
|
|
|
@ -532,9 +532,21 @@ module Gitlab
|
|||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# Omitted because no user, creator or author associated: `ci_runners`
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def usage_activity_by_stage_verify(time_period)
|
||||
{}
|
||||
{
|
||||
ci_builds: distinct_count(::Ci::Build.where(time_period), :user_id),
|
||||
ci_external_pipelines: distinct_count(::Ci::Pipeline.external.where(time_period), :user_id, start: user_minimum_id, finish: user_maximum_id),
|
||||
ci_internal_pipelines: distinct_count(::Ci::Pipeline.internal.where(time_period), :user_id, start: user_minimum_id, finish: user_maximum_id),
|
||||
ci_pipeline_config_auto_devops: distinct_count(::Ci::Pipeline.auto_devops_source.where(time_period), :user_id, start: user_minimum_id, finish: user_maximum_id),
|
||||
ci_pipeline_config_repository: distinct_count(::Ci::Pipeline.repository_source.where(time_period), :user_id, start: user_minimum_id, finish: user_maximum_id),
|
||||
ci_pipeline_schedules: distinct_count(::Ci::PipelineSchedule.where(time_period), :owner_id),
|
||||
ci_pipelines: distinct_count(::Ci::Pipeline.where(time_period), :user_id, start: user_minimum_id, finish: user_maximum_id),
|
||||
ci_triggers: distinct_count(::Ci::Trigger.where(time_period), :owner_id),
|
||||
clusters_applications_runner: cluster_applications_user_distinct_count(::Clusters::Applications::Runner, time_period)
|
||||
}
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# Currently too complicated and to get reliable counts for these stats:
|
||||
# container_scanning_jobs, dast_jobs, dependency_scanning_jobs, license_management_jobs, sast_jobs, secret_detection_jobs
|
||||
|
|
|
@ -23533,6 +23533,9 @@ msgstr ""
|
|||
msgid "This content could not be displayed because %{reason}. You can %{options} instead."
|
||||
msgstr ""
|
||||
|
||||
msgid "This credential has expired"
|
||||
msgstr ""
|
||||
|
||||
msgid "This date is after the due date, so this epic won't appear in the roadmap."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ RSpec.describe 'User sees user popover', :js do
|
|||
subject { page }
|
||||
|
||||
describe 'hovering over a user link in a merge request' do
|
||||
let(:popover_selector) { '.user-popover' }
|
||||
let(:popover_selector) { '[data-testid="user-popover"]' }
|
||||
|
||||
before do
|
||||
visit project_merge_request_path(project, merge_request)
|
||||
|
|
|
@ -7,22 +7,23 @@ export * from '@gitlab/ui';
|
|||
*
|
||||
* This mock decouples those tests from the implementation, removing the need to set
|
||||
* them up specially just for these tooltips.
|
||||
*
|
||||
* Mocking the modules using the full file path allows the mocks to take effect
|
||||
* when the modules are imported in this project (`gitlab`) **and** when they
|
||||
* are imported internally in `@gitlab/ui`.
|
||||
*/
|
||||
export const GlTooltipDirective = {
|
||||
bind() {},
|
||||
};
|
||||
|
||||
export const GlTooltip = {
|
||||
jest.mock('@gitlab/ui/dist/directives/tooltip.js', () => ({
|
||||
bind() {},
|
||||
}));
|
||||
|
||||
jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => ({
|
||||
render(h) {
|
||||
return h('div', this.$attrs, this.$slots.default);
|
||||
},
|
||||
};
|
||||
}));
|
||||
|
||||
export const GlPopoverDirective = {
|
||||
bind() {},
|
||||
};
|
||||
|
||||
export const GlPopover = {
|
||||
jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({
|
||||
props: {
|
||||
cssClasses: {
|
||||
type: Array,
|
||||
|
@ -33,4 +34,4 @@ export const GlPopover = {
|
|||
render(h) {
|
||||
return h('div', this.$attrs, Object.keys(this.$slots).map(s => this.$slots[s]));
|
||||
},
|
||||
};
|
||||
}));
|
||||
|
|
50
spec/lib/api/entities/nuget/dependency_group_spec.rb
Normal file
50
spec/lib/api/entities/nuget/dependency_group_spec.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Entities::Nuget::DependencyGroup do
|
||||
let(:dependency_group) do
|
||||
{
|
||||
id: 'http://gitlab.com/Sandbox.App/1.0.0.json#dependencygroup',
|
||||
type: 'PackageDependencyGroup',
|
||||
target_framework: 'fwk test',
|
||||
dependencies: [
|
||||
{
|
||||
id: 'http://gitlab.com/Sandbox.App/1.0.0.json#dependency',
|
||||
type: 'PackageDependency',
|
||||
name: 'Dependency',
|
||||
range: '2.0.0'
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
let(:expected) do
|
||||
{
|
||||
'@id': 'http://gitlab.com/Sandbox.App/1.0.0.json#dependencygroup',
|
||||
'@type': 'PackageDependencyGroup',
|
||||
'targetFramework': 'fwk test',
|
||||
'dependencies': [
|
||||
{
|
||||
'@id': 'http://gitlab.com/Sandbox.App/1.0.0.json#dependency',
|
||||
'@type': 'PackageDependency',
|
||||
'id': 'Dependency',
|
||||
'range': '2.0.0'
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
let(:entity) { described_class.new(dependency_group) }
|
||||
|
||||
subject { entity.as_json }
|
||||
|
||||
it { is_expected.to eq(expected) }
|
||||
|
||||
context 'dependency group without target framework' do
|
||||
let(:dependency_group_with_no_target_framework) { dependency_group.tap { |dg| dg[:target_framework] = nil } }
|
||||
let(:expected_no_target_framework) { expected.except(:targetFramework) }
|
||||
let(:entity) { described_class.new(dependency_group_with_no_target_framework) }
|
||||
|
||||
it { is_expected.to eq(expected_no_target_framework) }
|
||||
end
|
||||
end
|
28
spec/lib/api/entities/nuget/dependency_spec.rb
Normal file
28
spec/lib/api/entities/nuget/dependency_spec.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Entities::Nuget::Dependency do
|
||||
let(:dependency) do
|
||||
{
|
||||
id: 'http://gitlab.com/Sandbox.App/1.0.0.json#dependency',
|
||||
type: 'PackageDependency',
|
||||
name: 'Dependency',
|
||||
range: '2.0.0'
|
||||
}
|
||||
end
|
||||
|
||||
let(:expected) do
|
||||
{
|
||||
'@id': 'http://gitlab.com/Sandbox.App/1.0.0.json#dependency',
|
||||
'@type': 'PackageDependency',
|
||||
'id': 'Dependency',
|
||||
'range': '2.0.0'
|
||||
}
|
||||
end
|
||||
let(:entity) { described_class.new(dependency) }
|
||||
|
||||
subject { entity.as_json }
|
||||
|
||||
it { is_expected.to eq(expected) }
|
||||
end
|
35
spec/lib/api/entities/nuget/metadatum_spec.rb
Normal file
35
spec/lib/api/entities/nuget/metadatum_spec.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Entities::Nuget::Metadatum do
|
||||
let(:metadatum) do
|
||||
{
|
||||
project_url: 'http://sandbox.com/project',
|
||||
license_url: 'http://sandbox.com/license',
|
||||
icon_url: 'http://sandbox.com/icon'
|
||||
}
|
||||
end
|
||||
let(:expected) do
|
||||
{
|
||||
'projectUrl': 'http://sandbox.com/project',
|
||||
'licenseUrl': 'http://sandbox.com/license',
|
||||
'iconUrl': 'http://sandbox.com/icon'
|
||||
}
|
||||
end
|
||||
let(:entity) { described_class.new(metadatum) }
|
||||
|
||||
subject { entity.as_json }
|
||||
|
||||
it { is_expected.to eq(expected) }
|
||||
|
||||
%i[project_url license_url icon_url].each do |optional_field|
|
||||
context "metadatum without #{optional_field}" do
|
||||
let(:metadatum_without_a_field) { metadatum.except(optional_field) }
|
||||
let(:expected_without_a_field) { expected.except(optional_field.to_s.camelize(:lower).to_sym) }
|
||||
let(:entity) { described_class.new(metadatum_without_a_field) }
|
||||
|
||||
it { is_expected.to eq(expected_without_a_field) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Entities::Nuget::PackageMetadataCatalogEntry do
|
||||
let(:entry) do
|
||||
{
|
||||
json_url: 'http://sandbox.com/json/package',
|
||||
authors: 'Authors',
|
||||
dependency_groups: [],
|
||||
package_name: 'PackageTest',
|
||||
package_version: '1.2.3',
|
||||
tags: 'tag1 tag2 tag3',
|
||||
archive_url: 'http://sandbox.com/archive/package',
|
||||
summary: 'Summary',
|
||||
metadatum: {
|
||||
project_url: 'http://sandbox.com/project',
|
||||
license_url: 'http://sandbox.com/license',
|
||||
icon_url: 'http://sandbox.com/icon'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
let(:expected) do
|
||||
{
|
||||
'@id': 'http://sandbox.com/json/package',
|
||||
'id': 'PackageTest',
|
||||
'version': '1.2.3',
|
||||
'authors': 'Authors',
|
||||
'dependencyGroups': [],
|
||||
'tags': 'tag1 tag2 tag3',
|
||||
'packageContent': 'http://sandbox.com/archive/package',
|
||||
'summary': 'Summary',
|
||||
'projectUrl': 'http://sandbox.com/project',
|
||||
'licenseUrl': 'http://sandbox.com/license',
|
||||
'iconUrl': 'http://sandbox.com/icon'
|
||||
}
|
||||
end
|
||||
|
||||
subject { described_class.new(entry).as_json }
|
||||
|
||||
it { is_expected.to eq(expected) }
|
||||
end
|
57
spec/lib/api/entities/nuget/search_result_spec.rb
Normal file
57
spec/lib/api/entities/nuget/search_result_spec.rb
Normal file
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Entities::Nuget::SearchResult do
|
||||
let(:search_result) do
|
||||
{
|
||||
type: 'Package',
|
||||
authors: 'Author',
|
||||
name: 'PackageTest',
|
||||
version: '1.2.3',
|
||||
versions: [
|
||||
{
|
||||
json_url: 'http://sandbox.com/json/package',
|
||||
downloads: 100,
|
||||
version: '1.2.3'
|
||||
}
|
||||
],
|
||||
summary: 'Summary',
|
||||
total_downloads: 100,
|
||||
verified: true,
|
||||
tags: 'tag1 tag2 tag3',
|
||||
metadatum: {
|
||||
project_url: 'http://sandbox.com/project',
|
||||
license_url: 'http://sandbox.com/license',
|
||||
icon_url: 'http://sandbox.com/icon'
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:expected) do
|
||||
{
|
||||
'@type': 'Package',
|
||||
'authors': 'Author',
|
||||
'id': 'PackageTest',
|
||||
'title': 'PackageTest',
|
||||
'summary': 'Summary',
|
||||
'totalDownloads': 100,
|
||||
'verified': true,
|
||||
'version': '1.2.3',
|
||||
'tags': 'tag1 tag2 tag3',
|
||||
'projectUrl': 'http://sandbox.com/project',
|
||||
'licenseUrl': 'http://sandbox.com/license',
|
||||
'iconUrl': 'http://sandbox.com/icon',
|
||||
'versions': [
|
||||
{
|
||||
'@id': 'http://sandbox.com/json/package',
|
||||
'downloads': 100,
|
||||
'version': '1.2.3'
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
subject { described_class.new(search_result).as_json }
|
||||
|
||||
it { is_expected.to eq(expected) }
|
||||
end
|
|
@ -119,6 +119,45 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
]
|
||||
end
|
||||
end
|
||||
|
||||
context 'for verify' do
|
||||
it 'includes accurate usage_activity_by_stage data' do
|
||||
for_defined_days_back do
|
||||
user = create(:user)
|
||||
create(:ci_build, user: user)
|
||||
create(:ci_empty_pipeline, source: :external, user: user)
|
||||
create(:ci_empty_pipeline, user: user)
|
||||
create(:ci_pipeline, :auto_devops_source, user: user)
|
||||
create(:ci_pipeline, :repository_source, user: user)
|
||||
create(:ci_pipeline_schedule, owner: user)
|
||||
create(:ci_trigger, owner: user)
|
||||
create(:clusters_applications_runner, :installed)
|
||||
end
|
||||
|
||||
expect(described_class.uncached_data[:usage_activity_by_stage][:verify]).to include(
|
||||
ci_builds: 2,
|
||||
ci_external_pipelines: 2,
|
||||
ci_internal_pipelines: 2,
|
||||
ci_pipeline_config_auto_devops: 2,
|
||||
ci_pipeline_config_repository: 2,
|
||||
ci_pipeline_schedules: 2,
|
||||
ci_pipelines: 2,
|
||||
ci_triggers: 2,
|
||||
clusters_applications_runner: 2
|
||||
)
|
||||
expect(described_class.uncached_data[:usage_activity_by_stage_monthly][:verify]).to include(
|
||||
ci_builds: 1,
|
||||
ci_external_pipelines: 1,
|
||||
ci_internal_pipelines: 1,
|
||||
ci_pipeline_config_auto_devops: 1,
|
||||
ci_pipeline_config_repository: 1,
|
||||
ci_pipeline_schedules: 1,
|
||||
ci_pipelines: 1,
|
||||
ci_triggers: 1,
|
||||
clusters_applications_runner: 1
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for create' do
|
||||
|
|
238
spec/migrations/remove_duplicate_labels_from_project_spec.rb
Normal file
238
spec/migrations/remove_duplicate_labels_from_project_spec.rb
Normal file
|
@ -0,0 +1,238 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('db', 'post_migrate', '20200305082754_remove_duplicate_labels_from_project.rb')
|
||||
|
||||
RSpec.describe RemoveDuplicateLabelsFromProject do
|
||||
let(:labels_table) { table(:labels) }
|
||||
let(:labels) { labels_table.all }
|
||||
let(:projects_table) { table(:projects) }
|
||||
let(:projects) { projects_table.all }
|
||||
let(:namespaces_table) { table(:namespaces) }
|
||||
let(:namespaces) { namespaces_table.all }
|
||||
let(:backup_labels_table) { table(:backup_labels) }
|
||||
let(:backup_labels) { backup_labels_table.all }
|
||||
|
||||
# all the possible tables with records that may have a relationship with a label
|
||||
let(:analytics_cycle_analytics_group_stages_table) { table(:analytics_cycle_analytics_group_stages) }
|
||||
let(:analytics_cycle_analytics_project_stages_table) { table(:analytics_cycle_analytics_project_stages) }
|
||||
let(:board_labels_table) { table(:board_labels) }
|
||||
let(:label_links_table) { table(:label_links) }
|
||||
let(:label_priorities_table) { table(:label_priorities) }
|
||||
let(:lists_table) { table(:lists) }
|
||||
let(:resource_label_events_table) { table(:resource_label_events) }
|
||||
|
||||
let!(:group_one) { namespaces_table.create!(id: 1, type: 'Group', name: 'group', path: 'group') }
|
||||
let!(:project_one) do
|
||||
projects_table.create!(id: 1, name: 'project', path: 'project',
|
||||
visibility_level: 0, namespace_id: group_one.id)
|
||||
end
|
||||
let(:label_title) { 'bug' }
|
||||
let(:label_color) { 'red' }
|
||||
let(:label_description) { 'nice label' }
|
||||
let(:group_id) { group_one.id }
|
||||
let(:project_id) { project_one.id }
|
||||
let(:other_title) { 'feature' }
|
||||
|
||||
let(:group_label_attributes) do
|
||||
{
|
||||
title: label_title, color: label_color, group_id: group_id, type: 'GroupLabel', template: false, description: label_description
|
||||
}
|
||||
end
|
||||
|
||||
let(:project_label_attributes) do
|
||||
{
|
||||
title: label_title, color: label_color, project_id: project_id, type: 'ProjectLabel', template: false, description: label_description
|
||||
}
|
||||
end
|
||||
|
||||
let(:migration) { described_class.new }
|
||||
|
||||
describe 'removing full duplicates' do
|
||||
context 'when there are no duplicate labels' do
|
||||
let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1, title: "a different label")) }
|
||||
let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2, title: "a totally different label")) }
|
||||
|
||||
it 'does not remove anything' do
|
||||
expect { migration.up }.not_to change { backup_labels_table.count }
|
||||
end
|
||||
|
||||
it 'restores removed records when rolling back - no change' do
|
||||
migration.up
|
||||
|
||||
expect { migration.down }.not_to change { labels_table.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicates with no relationships' do
|
||||
# can't use the activerecord class because the `type` makes it think it has polymorphism and should be/have a ProjectLabel subclass
|
||||
let(:backup_labels) { ApplicationRecord.connection.execute('SELECT * from backup_labels') }
|
||||
|
||||
let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1)) }
|
||||
let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2)) }
|
||||
let!(:third_label) { labels_table.create!(project_label_attributes.merge(id: 3, title: other_title)) }
|
||||
let!(:fourth_label) { labels_table.create!(project_label_attributes.merge(id: 4, title: other_title)) }
|
||||
|
||||
it 'creates a backup record for each removed record' do
|
||||
expect { migration.up }.to change { backup_labels_table.count }.from(0).to(2)
|
||||
end
|
||||
|
||||
it 'creates the correct backup records with `create` restore_action' do
|
||||
migration.up
|
||||
|
||||
expect(backup_labels.find { |bl| bl["id"] == 2 }).to include(second_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
|
||||
expect(backup_labels.find { |bl| bl["id"] == 4 }).to include(fourth_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
|
||||
end
|
||||
|
||||
it 'deletes all but one' do
|
||||
migration.up
|
||||
|
||||
expect { second_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { fourth_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'restores removed records on rollback' do
|
||||
second_label_attributes = modified_attributes(second_label)
|
||||
fourth_label_attributes = modified_attributes(fourth_label)
|
||||
|
||||
migration.up
|
||||
|
||||
migration.down
|
||||
|
||||
expect(second_label.attributes).to include(second_label_attributes)
|
||||
expect(fourth_label.attributes).to include(fourth_label_attributes)
|
||||
end
|
||||
end
|
||||
|
||||
context 'two duplicate records, one of which has a relationship' do
|
||||
let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1)) }
|
||||
let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2)) }
|
||||
let!(:label_priority) { label_priorities_table.create!(label_id: second_label.id, project_id: project_id, priority: 1) }
|
||||
|
||||
it 'does not remove anything' do
|
||||
expect { migration.up }.not_to change { labels_table.count }
|
||||
end
|
||||
|
||||
it 'does not create a backup record with `create` restore_action' do
|
||||
expect { migration.up }.not_to change { backup_labels_table.where(restore_action: described_class::CREATE).count }
|
||||
end
|
||||
|
||||
it 'restores removed records when rolling back - no change' do
|
||||
migration.up
|
||||
|
||||
expect { migration.down }.not_to change { labels_table.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'multiple duplicates, a subset of which have relationships' do
|
||||
let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1)) }
|
||||
let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2)) }
|
||||
let!(:label_priority_for_second_label) { label_priorities_table.create!(label_id: second_label.id, project_id: project_id, priority: 1) }
|
||||
let!(:third_label) { labels_table.create!(project_label_attributes.merge(id: 3)) }
|
||||
let!(:fourth_label) { labels_table.create!(project_label_attributes.merge(id: 4)) }
|
||||
let!(:label_priority_for_fourth_label) { label_priorities_table.create!(label_id: fourth_label.id, project_id: project_id, priority: 2) }
|
||||
|
||||
it 'creates a backup record with `create` restore_action for each removed record' do
|
||||
expect { migration.up }.to change { backup_labels_table.where(restore_action: described_class::CREATE).count }.from(0).to(1)
|
||||
end
|
||||
|
||||
it 'creates the correct backup records' do
|
||||
migration.up
|
||||
|
||||
# can't use the activerecord class because the `type` column makes it think it has polymorphism and should be/have a ProjectLabel subclass
|
||||
backup_labels = ApplicationRecord.connection.execute('SELECT * from backup_labels')
|
||||
|
||||
expect(backup_labels.find { |bl| bl["id"] == 3 }).to include(third_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
|
||||
end
|
||||
|
||||
it 'deletes the duplicate record' do
|
||||
migration.up
|
||||
|
||||
expect { first_label.reload }.not_to raise_error
|
||||
expect { second_label.reload }.not_to raise_error
|
||||
expect { third_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'restores removed records on rollback' do
|
||||
third_label_attributes = modified_attributes(third_label)
|
||||
|
||||
migration.up
|
||||
migration.down
|
||||
|
||||
expect(third_label.attributes).to include(third_label_attributes)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'renaming partial duplicates' do
|
||||
# partial duplicates - only project_id and title match. Distinct colour prevents deletion.
|
||||
context 'when there are no duplicate labels' do
|
||||
let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1, title: "a unique label", color: 'green')) }
|
||||
let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2, title: "a totally different, unique, label", color: 'blue')) }
|
||||
|
||||
it 'does not rename anything' do
|
||||
expect { migration.up }.not_to change { backup_labels_table.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicates with no relationships' do
|
||||
let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1, color: 'green')) }
|
||||
let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2, color: 'blue')) }
|
||||
let!(:third_label) { labels_table.create!(project_label_attributes.merge(id: 3, title: other_title, color: 'purple')) }
|
||||
let!(:fourth_label) { labels_table.create!(project_label_attributes.merge(id: 4, title: other_title, color: 'yellow')) }
|
||||
|
||||
it 'creates a backup record for each renamed record' do
|
||||
expect { migration.up }.to change { backup_labels_table.count }.from(0).to(2)
|
||||
end
|
||||
|
||||
it 'creates the correct backup records with `rename` restore_action' do
|
||||
migration.up
|
||||
|
||||
# can't use the activerecord class because the `type` makes it think it has polymorphism and should be/have a ProjectLabel subclass
|
||||
backup_labels = ApplicationRecord.connection.execute('SELECT * from backup_labels')
|
||||
|
||||
expect(backup_labels.find { |bl| bl["id"] == 2 }).to include(second_label.attributes.merge("restore_action" => described_class::RENAME, "created_at" => anything, "updated_at" => anything))
|
||||
expect(backup_labels.find { |bl| bl["id"] == 4 }).to include(fourth_label.attributes.merge("restore_action" => described_class::RENAME, "created_at" => anything, "updated_at" => anything))
|
||||
end
|
||||
|
||||
it 'modifies the titles of the partial duplicates' do
|
||||
migration.up
|
||||
|
||||
expect(second_label.reload.title).to match(/#{label_title}_duplicate#{second_label.id}$/)
|
||||
expect(fourth_label.reload.title).to match(/#{other_title}_duplicate#{fourth_label.id}$/)
|
||||
end
|
||||
|
||||
it 'restores renamed records on rollback' do
|
||||
second_label_attributes = modified_attributes(second_label)
|
||||
fourth_label_attributes = modified_attributes(fourth_label)
|
||||
|
||||
migration.up
|
||||
|
||||
migration.down
|
||||
|
||||
expect(second_label.reload.attributes).to include(second_label_attributes)
|
||||
expect(fourth_label.reload.attributes).to include(fourth_label_attributes)
|
||||
end
|
||||
|
||||
context 'when the labels have a long title that might overflow' do
|
||||
let(:long_title) { "a" * 255 }
|
||||
|
||||
before do
|
||||
first_label.update_attribute(:title, long_title)
|
||||
second_label.update_attribute(:title, long_title)
|
||||
end
|
||||
|
||||
it 'keeps the length within the limit' do
|
||||
migration.up
|
||||
|
||||
expect(second_label.reload.title).to eq("#{"a" * 244}_duplicate#{second_label.id}")
|
||||
expect(second_label.title.length).to eq 255
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def modified_attributes(label)
|
||||
label.attributes.except('created_at', 'updated_at')
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@ RSpec.describe AlertManagement::Alerts::UpdateService do
|
|||
let_it_be(:user_with_permissions) { create(:user) }
|
||||
let_it_be(:other_user_with_permissions) { create(:user) }
|
||||
let_it_be(:user_without_permissions) { create(:user) }
|
||||
let_it_be(:alert, reload: true) { create(:alert_management_alert) }
|
||||
let_it_be(:alert, reload: true) { create(:alert_management_alert, :triggered) }
|
||||
let_it_be(:project) { alert.project }
|
||||
|
||||
let(:current_user) { user_with_permissions }
|
||||
|
@ -28,6 +28,10 @@ RSpec.describe AlertManagement::Alerts::UpdateService do
|
|||
specify { expect { response }.not_to change(Note, :count) }
|
||||
end
|
||||
|
||||
shared_examples 'adds a system note' do
|
||||
specify { expect { response }.to change { alert.reload.notes.count }.by(1) }
|
||||
end
|
||||
|
||||
shared_examples 'error response' do |message|
|
||||
it_behaves_like 'does not add a todo'
|
||||
it_behaves_like 'does not add a system note'
|
||||
|
@ -86,10 +90,6 @@ RSpec.describe AlertManagement::Alerts::UpdateService do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'adds a system note' do
|
||||
specify { expect { response }.to change { alert.reload.notes.count }.by(1) }
|
||||
end
|
||||
|
||||
shared_examples 'successful assignment' do
|
||||
it_behaves_like 'adds a system note'
|
||||
it_behaves_like 'adds a todo'
|
||||
|
@ -143,5 +143,38 @@ RSpec.describe AlertManagement::Alerts::UpdateService do
|
|||
it_behaves_like 'successful assignment'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a status is included' do
|
||||
let(:params) { { status: new_status } }
|
||||
let(:new_status) { AlertManagement::Alert::STATUSES[:acknowledged] }
|
||||
|
||||
it 'successfully changes the status' do
|
||||
expect { response }.to change { alert.acknowledged? }.to(true)
|
||||
expect(response).to be_success
|
||||
expect(response.payload[:alert]).to eq(alert)
|
||||
end
|
||||
|
||||
it_behaves_like 'adds a system note'
|
||||
|
||||
context 'with unknown status' do
|
||||
let(:new_status) { -1 }
|
||||
|
||||
it_behaves_like 'error response', 'Invalid status'
|
||||
end
|
||||
|
||||
context 'with resolving status' do
|
||||
let(:new_status) { AlertManagement::Alert::STATUSES[:resolved] }
|
||||
|
||||
it 'changes the status' do
|
||||
expect { response }.to change { alert.resolved? }.to(true)
|
||||
end
|
||||
|
||||
it "resolves the current user's related todos" do
|
||||
todo = create(:todo, :pending, target: alert, user: current_user, project: alert.project)
|
||||
|
||||
expect { response }.to change { todo.reload.state }.from('pending').to('done')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe AlertManagement::UpdateAlertStatusService do
|
||||
let(:project) { alert.project }
|
||||
let_it_be(:user) { build(:user) }
|
||||
|
||||
let_it_be(:alert, reload: true) do
|
||||
create(:alert_management_alert, :triggered)
|
||||
end
|
||||
|
||||
let(:service) { described_class.new(alert, user, new_status) }
|
||||
|
||||
describe '#execute' do
|
||||
shared_examples 'update failure' do |error_message|
|
||||
it 'returns an error' do
|
||||
expect(response).to be_error
|
||||
expect(response.message).to eq(error_message)
|
||||
expect(response.payload[:alert]).to eq(alert)
|
||||
end
|
||||
|
||||
it 'does not update the status' do
|
||||
expect { response }.not_to change { alert.status }
|
||||
end
|
||||
end
|
||||
|
||||
let(:new_status) { AlertManagement::Alert::STATUSES[:acknowledged] }
|
||||
let(:can_update) { true }
|
||||
|
||||
subject(:response) { service.execute }
|
||||
|
||||
before do
|
||||
allow(user).to receive(:can?)
|
||||
.with(:update_alert_management_alert, project)
|
||||
.and_return(can_update)
|
||||
end
|
||||
|
||||
it 'returns success' do
|
||||
expect(response).to be_success
|
||||
expect(response.payload[:alert]).to eq(alert)
|
||||
end
|
||||
|
||||
it 'updates the status' do
|
||||
expect { response }.to change { alert.acknowledged? }.to(true)
|
||||
end
|
||||
|
||||
context 'resolving status' do
|
||||
let(:new_status) { AlertManagement::Alert::STATUSES[:resolved] }
|
||||
|
||||
it 'updates the status' do
|
||||
expect { response }.to change { alert.resolved? }.to(true)
|
||||
end
|
||||
|
||||
context 'user has a pending todo' do
|
||||
let(:user) { create(:user) }
|
||||
let!(:todo) { create(:todo, :pending, target: alert, user: user, project: alert.project) }
|
||||
|
||||
it 'resolves the todo' do
|
||||
expect { response }.to change { todo.reload.state }.from('pending').to('done')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has no permissions' do
|
||||
let(:can_update) { false }
|
||||
|
||||
include_examples 'update failure', _('You have no permissions')
|
||||
end
|
||||
|
||||
context 'with no status' do
|
||||
let(:new_status) { nil }
|
||||
|
||||
include_examples 'update failure', _('Invalid status')
|
||||
end
|
||||
|
||||
context 'with unknown status' do
|
||||
let(:new_status) { -1 }
|
||||
|
||||
include_examples 'update failure', _('Invalid status')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -54,29 +54,5 @@ RSpec.describe IncidentManagement::CreateIncidentLabelService do
|
|||
context 'without label' do
|
||||
it_behaves_like 'new label'
|
||||
end
|
||||
|
||||
context 'with duplicate labels', issue: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/65042' do
|
||||
before do
|
||||
# Replicate race condition to create duplicates
|
||||
build(:label, project: project, title: title).save!(validate: false)
|
||||
build(:label, project: project, title: title).save!(validate: false)
|
||||
end
|
||||
|
||||
it 'create an issue without labels' do
|
||||
# Verify we have duplicates
|
||||
expect(project.labels.size).to eq(2)
|
||||
expect(project.labels.map(&:title)).to all(eq(title))
|
||||
|
||||
message = <<~MESSAGE.chomp
|
||||
Cannot create incident label "incident" \
|
||||
for "#{project.full_name}": Title has already been taken.
|
||||
MESSAGE
|
||||
|
||||
expect(service).to receive(:log_info).with(message)
|
||||
expect(execute).to be_error
|
||||
expect(execute.payload[:label]).to be_kind_of(Label)
|
||||
expect(execute.message).to eq('Title has already been taken')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -681,4 +681,16 @@ RSpec.describe SystemNoteService do
|
|||
described_class.unapprove_mr(noteable, author)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.change_alert_status' do
|
||||
let(:alert) { build(:alert_management_alert) }
|
||||
|
||||
it 'calls AlertManagementService' do
|
||||
expect_next_instance_of(SystemNotes::AlertManagementService) do |service|
|
||||
expect(service).to receive(:change_alert_status).with(alert)
|
||||
end
|
||||
|
||||
described_class.change_alert_status(alert, author)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
22
spec/services/system_notes/alert_management_service_spec.rb
Normal file
22
spec/services/system_notes/alert_management_service_spec.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ::SystemNotes::AlertManagementService do
|
||||
let_it_be(:author) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
|
||||
let(:noteable) { create(:alert_management_alert, :acknowledged, project: project) }
|
||||
|
||||
describe '#change_alert_status' do
|
||||
subject { described_class.new(noteable: noteable, project: project, author: author).change_alert_status(noteable) }
|
||||
|
||||
it_behaves_like 'a system note' do
|
||||
let(:action) { 'status' }
|
||||
end
|
||||
|
||||
it 'has the appropriate message' do
|
||||
expect(subject.note).to eq("changed the status to **Acknowledged**")
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue