Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bc935f05bc
commit
43c3400c67
31 changed files with 483 additions and 240 deletions
|
@ -117,7 +117,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<section class="media-section mr-widget-border-top" data-testid="widget-extension">
|
||||
<section class="media-section" data-testid="widget-extension">
|
||||
<div class="media gl-p-5">
|
||||
<status-icon
|
||||
:name="$options.name"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { __ } from '~/locale';
|
||||
import { registeredExtensions } from './index';
|
||||
|
||||
export default {
|
||||
|
@ -12,23 +13,42 @@ export default {
|
|||
|
||||
if (extensions.length === 0) return null;
|
||||
|
||||
return h('div', {}, [
|
||||
...extensions.map((extension) =>
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
attrs: {
|
||||
role: 'region',
|
||||
'aria-label': __('Merge request reports'),
|
||||
},
|
||||
},
|
||||
[
|
||||
h(
|
||||
{ ...extension },
|
||||
'ul',
|
||||
{
|
||||
props: {
|
||||
...extension.props.reduce(
|
||||
(acc, key) => ({
|
||||
...acc,
|
||||
[key]: this.mr[key],
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
class: 'gl-p-0 gl-m-0 gl-list-style-none',
|
||||
},
|
||||
[
|
||||
...extensions.map((extension, index) =>
|
||||
h('li', { attrs: { class: index > 0 && 'mr-widget-border-top' } }, [
|
||||
h(
|
||||
{ ...extension },
|
||||
{
|
||||
props: {
|
||||
...extension.props.reduce(
|
||||
(acc, key) => ({
|
||||
...acc,
|
||||
[key]: this.mr[key],
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
],
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ class SearchController < ApplicationController
|
|||
around_action :allow_gitaly_ref_name_caching
|
||||
|
||||
before_action :block_anonymous_global_searches, :check_scope_global_search_enabled, except: :opensearch
|
||||
before_action :strip_surrounding_whitespace_from_search, except: :opensearch
|
||||
skip_before_action :authenticate_user!
|
||||
requires_cross_project_access if: -> do
|
||||
search_term_present = params[:search].present? || params[:term].present?
|
||||
|
@ -197,6 +198,10 @@ class SearchController < ApplicationController
|
|||
def count_action_name?
|
||||
action_name.to_sym == :count
|
||||
end
|
||||
|
||||
def strip_surrounding_whitespace_from_search
|
||||
%i(term search).each { |param| params[param]&.strip! }
|
||||
end
|
||||
end
|
||||
|
||||
SearchController.prepend_mod_with('SearchController')
|
||||
|
|
|
@ -10,6 +10,8 @@ class ProtectedBranch < ApplicationRecord
|
|||
scope :allowing_force_push,
|
||||
-> { where(allow_force_push: true) }
|
||||
|
||||
scope :get_ids_by_name, -> (name) { where(name: name).pluck(:id) }
|
||||
|
||||
protected_ref_access_levels :merge, :push
|
||||
|
||||
def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
|
||||
|
|
|
@ -457,6 +457,7 @@ class User < ApplicationRecord
|
|||
scope :dormant, -> { active.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) }
|
||||
scope :with_no_activity, -> { active.where(last_activity_on: nil) }
|
||||
scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) }
|
||||
scope :get_ids_by_username, -> (username) { where(username: username).pluck(:id) }
|
||||
|
||||
def preferred_language
|
||||
read_attribute('preferred_language') ||
|
||||
|
|
|
@ -24,8 +24,8 @@ module ContainerExpirationPolicies
|
|||
|
||||
begin
|
||||
service_result = Projects::ContainerRepository::CleanupTagsService
|
||||
.new(project, nil, policy_params.merge('container_expiration_policy' => true))
|
||||
.execute(repository)
|
||||
.new(repository, nil, policy_params.merge('container_expiration_policy' => true))
|
||||
.execute
|
||||
rescue StandardError
|
||||
repository.cleanup_unfinished!
|
||||
|
||||
|
|
|
@ -91,11 +91,11 @@ module Issues
|
|||
end
|
||||
end
|
||||
|
||||
def store_first_mentioned_in_commit_at(issue, merge_request)
|
||||
def store_first_mentioned_in_commit_at(issue, merge_request, max_commit_lookup: 100)
|
||||
metrics = issue.metrics
|
||||
return if metrics.nil? || metrics.first_mentioned_in_commit_at
|
||||
|
||||
first_commit_timestamp = merge_request.commits(limit: 1).first.try(:authored_date)
|
||||
first_commit_timestamp = merge_request.commits(limit: max_commit_lookup).last.try(:authored_date)
|
||||
return unless first_commit_timestamp
|
||||
|
||||
metrics.update!(first_mentioned_in_commit_at: first_commit_timestamp)
|
||||
|
|
|
@ -2,148 +2,152 @@
|
|||
|
||||
module Projects
|
||||
module ContainerRepository
|
||||
class CleanupTagsService < BaseService
|
||||
class CleanupTagsService
|
||||
include BaseServiceUtility
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
|
||||
def execute(container_repository)
|
||||
def initialize(container_repository, user = nil, params = {})
|
||||
@container_repository = container_repository
|
||||
@current_user = user
|
||||
@params = params.dup
|
||||
|
||||
@project = container_repository.project
|
||||
@tags = container_repository.tags
|
||||
tags_size = @tags.size
|
||||
@counts = {
|
||||
original_size: tags_size,
|
||||
cached_tags_count: 0
|
||||
}
|
||||
end
|
||||
|
||||
def execute
|
||||
return error('access denied') unless can_destroy?
|
||||
return error('invalid regex') unless valid_regex?
|
||||
|
||||
tags = container_repository.tags
|
||||
original_size = tags.size
|
||||
filter_out_latest
|
||||
filter_by_name
|
||||
|
||||
tags = without_latest(tags)
|
||||
tags = filter_by_name(tags)
|
||||
truncate
|
||||
populate_from_cache
|
||||
|
||||
before_truncate_size = tags.size
|
||||
tags = truncate(tags)
|
||||
after_truncate_size = tags.size
|
||||
filter_keep_n
|
||||
filter_by_older_than
|
||||
|
||||
cached_tags_count = populate_tags_from_cache(container_repository, tags) || 0
|
||||
|
||||
tags = filter_keep_n(container_repository, tags)
|
||||
tags = filter_by_older_than(container_repository, tags)
|
||||
|
||||
delete_tags(container_repository, tags).tap do |result|
|
||||
result[:original_size] = original_size
|
||||
result[:before_truncate_size] = before_truncate_size
|
||||
result[:after_truncate_size] = after_truncate_size
|
||||
result[:cached_tags_count] = cached_tags_count
|
||||
result[:before_delete_size] = tags.size
|
||||
delete_tags.merge(@counts).tap do |result|
|
||||
result[:before_delete_size] = @tags.size
|
||||
result[:deleted_size] = result[:deleted]&.size
|
||||
|
||||
result[:status] = :error if before_truncate_size != after_truncate_size
|
||||
result[:status] = :error if @counts[:before_truncate_size] != @counts[:after_truncate_size]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def delete_tags(container_repository, tags)
|
||||
return success(deleted: []) unless tags.any?
|
||||
|
||||
tag_names = tags.map(&:name)
|
||||
def delete_tags
|
||||
return success(deleted: []) unless @tags.any?
|
||||
|
||||
service = Projects::ContainerRepository::DeleteTagsService.new(
|
||||
container_repository.project,
|
||||
current_user,
|
||||
tags: tag_names,
|
||||
container_expiration_policy: params['container_expiration_policy']
|
||||
@project,
|
||||
@current_user,
|
||||
tags: @tags.map(&:name),
|
||||
container_expiration_policy: container_expiration_policy
|
||||
)
|
||||
|
||||
service.execute(container_repository)
|
||||
service.execute(@container_repository)
|
||||
end
|
||||
|
||||
def without_latest(tags)
|
||||
tags.reject(&:latest?)
|
||||
def filter_out_latest
|
||||
@tags.reject!(&:latest?)
|
||||
end
|
||||
|
||||
def order_by_date(tags)
|
||||
def order_by_date
|
||||
now = DateTime.current
|
||||
tags.sort_by { |tag| tag.created_at || now }.reverse
|
||||
@tags.sort_by! { |tag| tag.created_at || now }
|
||||
.reverse!
|
||||
end
|
||||
|
||||
def filter_by_name(tags)
|
||||
regex_delete = ::Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_delete'] || params['name_regex']}\\z")
|
||||
regex_retain = ::Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_keep']}\\z")
|
||||
def filter_by_name
|
||||
regex_delete = ::Gitlab::UntrustedRegexp.new("\\A#{name_regex_delete || name_regex}\\z")
|
||||
regex_retain = ::Gitlab::UntrustedRegexp.new("\\A#{name_regex_keep}\\z")
|
||||
|
||||
tags.select do |tag|
|
||||
@tags.select! do |tag|
|
||||
# regex_retain will override any overlapping matches by regex_delete
|
||||
regex_delete.match?(tag.name) && !regex_retain.match?(tag.name)
|
||||
end
|
||||
end
|
||||
|
||||
def filter_keep_n(container_repository, tags)
|
||||
return tags unless params['keep_n']
|
||||
def filter_keep_n
|
||||
return unless keep_n
|
||||
|
||||
tags = order_by_date(tags)
|
||||
cache_tags(container_repository, tags.first(keep_n))
|
||||
tags.drop(keep_n)
|
||||
order_by_date
|
||||
cache_tags(@tags.first(keep_n_as_integer))
|
||||
@tags = @tags.drop(keep_n_as_integer)
|
||||
end
|
||||
|
||||
def filter_by_older_than(container_repository, tags)
|
||||
return tags unless older_than
|
||||
def filter_by_older_than
|
||||
return unless older_than
|
||||
|
||||
older_than_timestamp = older_than_in_seconds.ago
|
||||
|
||||
tags, tags_to_keep = tags.partition do |tag|
|
||||
@tags, tags_to_keep = @tags.partition do |tag|
|
||||
tag.created_at && tag.created_at < older_than_timestamp
|
||||
end
|
||||
|
||||
cache_tags(container_repository, tags_to_keep)
|
||||
|
||||
tags
|
||||
cache_tags(tags_to_keep)
|
||||
end
|
||||
|
||||
def can_destroy?
|
||||
return true if params['container_expiration_policy']
|
||||
return true if container_expiration_policy
|
||||
|
||||
can?(current_user, :destroy_container_image, project)
|
||||
can?(@current_user, :destroy_container_image, @project)
|
||||
end
|
||||
|
||||
def valid_regex?
|
||||
%w(name_regex_delete name_regex name_regex_keep).each do |param_name|
|
||||
regex = params[param_name]
|
||||
regex = @params[param_name]
|
||||
::Gitlab::UntrustedRegexp.new(regex) unless regex.blank?
|
||||
end
|
||||
true
|
||||
rescue RegexpError => e
|
||||
::Gitlab::ErrorTracking.log_exception(e, project_id: project.id)
|
||||
::Gitlab::ErrorTracking.log_exception(e, project_id: @project.id)
|
||||
false
|
||||
end
|
||||
|
||||
def truncate(tags)
|
||||
return tags unless throttling_enabled?
|
||||
return tags if max_list_size == 0
|
||||
def truncate
|
||||
@counts[:before_truncate_size] = @tags.size
|
||||
@counts[:after_truncate_size] = @tags.size
|
||||
|
||||
return unless throttling_enabled?
|
||||
return if max_list_size == 0
|
||||
|
||||
# truncate the list to make sure that after the #filter_keep_n
|
||||
# execution, the resulting list will be max_list_size
|
||||
truncated_size = max_list_size + keep_n
|
||||
truncated_size = max_list_size + keep_n_as_integer
|
||||
|
||||
return tags if tags.size <= truncated_size
|
||||
return if @tags.size <= truncated_size
|
||||
|
||||
tags.sample(truncated_size)
|
||||
@tags = @tags.sample(truncated_size)
|
||||
@counts[:after_truncate_size] = @tags.size
|
||||
end
|
||||
|
||||
def populate_tags_from_cache(container_repository, tags)
|
||||
cache(container_repository).populate(tags) if caching_enabled?(container_repository)
|
||||
def populate_from_cache
|
||||
@counts[:cached_tags_count] = cache.populate(@tags) if caching_enabled?
|
||||
end
|
||||
|
||||
def cache_tags(container_repository, tags)
|
||||
cache(container_repository).insert(tags, older_than_in_seconds) if caching_enabled?(container_repository)
|
||||
def cache_tags(tags)
|
||||
cache.insert(tags, older_than_in_seconds) if caching_enabled?
|
||||
end
|
||||
|
||||
def cache(container_repository)
|
||||
# TODO Implement https://gitlab.com/gitlab-org/gitlab/-/issues/340277 to avoid passing
|
||||
# the container repository parameter which is bad for a memoized function
|
||||
def cache
|
||||
strong_memoize(:cache) do
|
||||
::Projects::ContainerRepository::CacheTagsCreatedAtService.new(container_repository)
|
||||
::Projects::ContainerRepository::CacheTagsCreatedAtService.new(@container_repository)
|
||||
end
|
||||
end
|
||||
|
||||
def caching_enabled?(container_repository)
|
||||
params['container_expiration_policy'] &&
|
||||
def caching_enabled?
|
||||
container_expiration_policy &&
|
||||
older_than.present? &&
|
||||
Feature.enabled?(:container_registry_expiration_policies_caching, container_repository.project)
|
||||
Feature.enabled?(:container_registry_expiration_policies_caching, @project)
|
||||
end
|
||||
|
||||
def throttling_enabled?
|
||||
|
@ -155,7 +159,11 @@ module Projects
|
|||
end
|
||||
|
||||
def keep_n
|
||||
params['keep_n'].to_i
|
||||
@params['keep_n']
|
||||
end
|
||||
|
||||
def keep_n_as_integer
|
||||
keep_n.to_i
|
||||
end
|
||||
|
||||
def older_than_in_seconds
|
||||
|
@ -165,7 +173,23 @@ module Projects
|
|||
end
|
||||
|
||||
def older_than
|
||||
params['older_than']
|
||||
@params['older_than']
|
||||
end
|
||||
|
||||
def name_regex_delete
|
||||
@params['name_regex_delete']
|
||||
end
|
||||
|
||||
def name_regex
|
||||
@params['name_regex']
|
||||
end
|
||||
|
||||
def name_regex_keep
|
||||
@params['name_regex_keep']
|
||||
end
|
||||
|
||||
def container_expiration_policy
|
||||
@params['container_expiration_policy']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,7 @@ module Ci
|
|||
module StuckBuilds
|
||||
class DropRunningWorker
|
||||
include ApplicationWorker
|
||||
include ExclusiveLeaseGuard
|
||||
|
||||
idempotent!
|
||||
|
||||
|
@ -17,26 +18,16 @@ module Ci
|
|||
|
||||
feature_category :continuous_integration
|
||||
|
||||
EXCLUSIVE_LEASE_KEY = 'ci_stuck_builds_drop_running_worker_lease'
|
||||
|
||||
def perform
|
||||
return unless try_obtain_lease
|
||||
|
||||
begin
|
||||
try_obtain_lease do
|
||||
Ci::StuckBuilds::DropRunningService.new.execute
|
||||
ensure
|
||||
remove_lease
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def try_obtain_lease
|
||||
@uuid = Gitlab::ExclusiveLease.new(EXCLUSIVE_LEASE_KEY, timeout: 30.minutes).try_obtain
|
||||
end
|
||||
|
||||
def remove_lease
|
||||
Gitlab::ExclusiveLease.cancel(EXCLUSIVE_LEASE_KEY, @uuid)
|
||||
def lease_timeout
|
||||
30.minutes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,8 +28,8 @@ class CleanupContainerRepositoryWorker
|
|||
end
|
||||
|
||||
result = Projects::ContainerRepository::CleanupTagsService
|
||||
.new(project, current_user, params)
|
||||
.execute(container_repository)
|
||||
.new(container_repository, current_user, params)
|
||||
.execute
|
||||
|
||||
if run_by_container_expiration_policy? && result[:status] == :success
|
||||
container_repository.reset_expiration_policy_started_at!
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class StuckCiJobsWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include ApplicationWorker
|
||||
include ExclusiveLeaseGuard
|
||||
|
||||
# rubocop:disable Scalability/CronWorkerContext
|
||||
# This is an instance-wide cleanup query, so there's no meaningful
|
||||
|
@ -14,28 +15,18 @@ class StuckCiJobsWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
feature_category :continuous_integration
|
||||
worker_resource_boundary :cpu
|
||||
|
||||
EXCLUSIVE_LEASE_KEY = 'stuck_ci_builds_worker_lease'
|
||||
|
||||
def perform
|
||||
Ci::StuckBuilds::DropRunningWorker.perform_in(20.minutes)
|
||||
Ci::StuckBuilds::DropScheduledWorker.perform_in(40.minutes)
|
||||
|
||||
return unless try_obtain_lease
|
||||
|
||||
begin
|
||||
try_obtain_lease do
|
||||
Ci::StuckBuilds::DropService.new.execute
|
||||
ensure
|
||||
remove_lease
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def try_obtain_lease
|
||||
@uuid = Gitlab::ExclusiveLease.new(EXCLUSIVE_LEASE_KEY, timeout: 30.minutes).try_obtain
|
||||
end
|
||||
|
||||
def remove_lease
|
||||
Gitlab::ExclusiveLease.cancel(EXCLUSIVE_LEASE_KEY, @uuid)
|
||||
def lease_timeout
|
||||
30.minutes
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTemporaryIndexToIssueMetrics < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_issue_metrics_first_mentioned_in_commit'
|
||||
|
||||
def up
|
||||
add_concurrent_index :issue_metrics, :issue_id, where: 'EXTRACT(YEAR FROM first_mentioned_in_commit_at) > 2019', name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :issue_metrics, name: INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ScheduleFixFirstMentionedInCommitAtJob < Gitlab::Database::Migration[1.0]
|
||||
MIGRATION = 'FixFirstMentionedInCommitAt'
|
||||
BATCH_SIZE = 10_000
|
||||
INTERVAL = 2.minutes.to_i
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
scope = define_batchable_model('issue_metrics')
|
||||
.where('EXTRACT(YEAR FROM first_mentioned_in_commit_at) > 2019')
|
||||
|
||||
queue_background_migration_jobs_by_range_at_intervals(
|
||||
scope,
|
||||
MIGRATION,
|
||||
INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
track_jobs: true,
|
||||
primary_column_name: :issue_id
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
# noop
|
||||
end
|
||||
end
|
1
db/schema_migrations/20211004110500
Normal file
1
db/schema_migrations/20211004110500
Normal file
|
@ -0,0 +1 @@
|
|||
1b0b562aefb724afe24b8640a22013cea6fddd0e594d6723f6819f69804ba9f7
|
1
db/schema_migrations/20211004110927
Normal file
1
db/schema_migrations/20211004110927
Normal file
|
@ -0,0 +1 @@
|
|||
50c937f979c83f6937364d92bf65ed42ef963f2d241eadcee6355c1b256c3ec9
|
|
@ -25448,6 +25448,8 @@ CREATE UNIQUE INDEX index_issue_links_on_source_id_and_target_id ON issue_links
|
|||
|
||||
CREATE INDEX index_issue_links_on_target_id ON issue_links USING btree (target_id);
|
||||
|
||||
CREATE INDEX index_issue_metrics_first_mentioned_in_commit ON issue_metrics USING btree (issue_id) WHERE (date_part('year'::text, first_mentioned_in_commit_at) > (2019)::double precision);
|
||||
|
||||
CREATE INDEX index_issue_metrics_on_issue_id_and_timestamps ON issue_metrics USING btree (issue_id, first_mentioned_in_commit_at, first_associated_with_milestone_at, first_added_to_board_at);
|
||||
|
||||
CREATE INDEX index_issue_on_project_id_state_id_and_blocking_issues_count ON issues USING btree (project_id, state_id, blocking_issues_count);
|
||||
|
|
|
@ -30,7 +30,7 @@ Prerequisites:
|
|||
To view the compliance report:
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Security & Compliance > Compliance**.
|
||||
1. On the left sidebar, select **Security & Compliance > Compliance report**.
|
||||
|
||||
NOTE:
|
||||
The compliance report shows only the latest merge request on each project.
|
||||
|
@ -87,7 +87,7 @@ Depending on the merge strategy, the merge commit SHA can be a merge commit, squ
|
|||
To download the Chain of Custody report:
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Security & Compliance > Compliance**.
|
||||
1. On the left sidebar, select **Security & Compliance > Compliance report**.
|
||||
1. Select **List of all merge commits**.
|
||||
|
||||
### Commit-specific Chain of Custody Report **(ULTIMATE)**
|
||||
|
@ -97,7 +97,7 @@ To download the Chain of Custody report:
|
|||
You can generate a commit-specific Chain of Custody report for a given commit SHA.
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Security & Compliance > Compliance**.
|
||||
1. On the left sidebar, select **Security & Compliance > Compliance report**.
|
||||
1. At the top of the compliance report, to the right of **List of all merge commits**, select the down arrow (**{angle-down}**).
|
||||
1. Enter the merge commit SHA, and then select **Export commit custody report**.
|
||||
SHA and then select **Export commit custody report**.
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# Class that fixes the incorrectly set authored_date within
|
||||
# issue_metrics table
|
||||
class FixFirstMentionedInCommitAt
|
||||
SUB_BATCH_SIZE = 500
|
||||
|
||||
# rubocop: disable Style/Documentation
|
||||
class TmpIssueMetrics < ActiveRecord::Base
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'issue_metrics'
|
||||
|
||||
def self.from_2020
|
||||
where('EXTRACT(YEAR FROM first_mentioned_in_commit_at) > 2019')
|
||||
end
|
||||
end
|
||||
# rubocop: enable Style/Documentation
|
||||
|
||||
def perform(start_id, end_id)
|
||||
scope(start_id, end_id).each_batch(of: SUB_BATCH_SIZE, column: :issue_id) do |sub_batch|
|
||||
first, last = sub_batch.pluck(Arel.sql('min(issue_id), max(issue_id)')).first
|
||||
|
||||
# The query need to be reconstructed because .each_batch modifies the default scope
|
||||
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/330510
|
||||
inner_query = TmpIssueMetrics
|
||||
.unscoped
|
||||
.merge(scope(first, last))
|
||||
.from("issue_metrics, #{lateral_query}")
|
||||
.select('issue_metrics.issue_id', 'first_authored_date.authored_date')
|
||||
.where('issue_metrics.first_mentioned_in_commit_at > first_authored_date.authored_date')
|
||||
|
||||
TmpIssueMetrics.connection.execute <<~UPDATE_METRICS
|
||||
WITH cte AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
|
||||
#{inner_query.to_sql}
|
||||
)
|
||||
UPDATE issue_metrics
|
||||
SET
|
||||
first_mentioned_in_commit_at = cte.authored_date
|
||||
FROM
|
||||
cte
|
||||
WHERE
|
||||
cte.issue_id = issue_metrics.issue_id
|
||||
UPDATE_METRICS
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scope(start_id, end_id)
|
||||
TmpIssueMetrics.from_2020.where(issue_id: start_id..end_id)
|
||||
end
|
||||
|
||||
def lateral_query
|
||||
<<~SQL
|
||||
LATERAL (
|
||||
SELECT MIN(first_authored_date.authored_date) as authored_date
|
||||
FROM merge_requests_closing_issues,
|
||||
LATERAL (
|
||||
SELECT id
|
||||
FROM merge_request_diffs
|
||||
WHERE merge_request_id = merge_requests_closing_issues.merge_request_id
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
) last_diff_id,
|
||||
LATERAL (
|
||||
SELECT authored_date
|
||||
FROM merge_request_diff_commits
|
||||
WHERE
|
||||
merge_request_diff_id = last_diff_id.id
|
||||
ORDER BY relative_order DESC
|
||||
LIMIT 1
|
||||
) first_authored_date
|
||||
WHERE merge_requests_closing_issues.issue_id = issue_metrics.issue_id
|
||||
) first_authored_date
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -106,7 +106,7 @@ module Gitlab
|
|||
final_delay = 0
|
||||
batch_counter = 0
|
||||
|
||||
model_class.each_batch(of: batch_size) do |relation, index|
|
||||
model_class.each_batch(of: batch_size, column: primary_column_name) do |relation, index|
|
||||
max = relation.arel_table[primary_column_name].maximum
|
||||
min = relation.arel_table[primary_column_name].minimum
|
||||
|
||||
|
|
|
@ -8460,15 +8460,12 @@ msgstr ""
|
|||
msgid "Completed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Compliance"
|
||||
msgstr ""
|
||||
|
||||
msgid "Compliance Dashboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "Compliance framework"
|
||||
msgstr ""
|
||||
|
||||
msgid "Compliance report"
|
||||
msgstr ""
|
||||
|
||||
msgid "ComplianceDashboard|created by:"
|
||||
msgstr ""
|
||||
|
||||
|
@ -21350,6 +21347,9 @@ msgstr ""
|
|||
msgid "Merge request events"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merge request reports"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merge request was scheduled to merge after pipeline succeeds"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -61,7 +61,10 @@ module QA
|
|||
end
|
||||
|
||||
context 'with project' do
|
||||
it 'successfully imports project' do
|
||||
it(
|
||||
'successfully imports project',
|
||||
testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/2297'
|
||||
) do
|
||||
expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration)
|
||||
|
||||
imported_projects = imported_group.reload!.projects
|
||||
|
|
|
@ -215,6 +215,16 @@ RSpec.describe SearchController do
|
|||
end
|
||||
end
|
||||
|
||||
it 'strips surrounding whitespace from search query' do
|
||||
get :show, params: { scope: 'notes', search: ' foobar ' }
|
||||
expect(assigns[:search_term]).to eq 'foobar'
|
||||
end
|
||||
|
||||
it 'strips surrounding whitespace from autocomplete term' do
|
||||
expect(controller).to receive(:search_autocomplete_opts).with('youcompleteme')
|
||||
get :autocomplete, params: { term: ' youcompleteme ' }
|
||||
end
|
||||
|
||||
it 'finds issue comments' do
|
||||
project = create(:project, :public)
|
||||
note = create(:note_on_issue, project: project)
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::FixFirstMentionedInCommitAt, :migration, schema: 20211004110500 do
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:projects) { table(:projects) }
|
||||
let(:users) { table(:users) }
|
||||
let(:merge_requests) { table(:merge_requests) }
|
||||
let(:issues) { table(:issues) }
|
||||
let(:issue_metrics) { table(:issue_metrics) }
|
||||
let(:merge_requests_closing_issues) { table(:merge_requests_closing_issues) }
|
||||
let(:diffs) { table(:merge_request_diffs) }
|
||||
let(:ten_days_ago) { 10.days.ago }
|
||||
let(:commits) do
|
||||
table(:merge_request_diff_commits).tap do |t|
|
||||
t.extend(SuppressCompositePrimaryKeyWarning)
|
||||
end
|
||||
end
|
||||
|
||||
let(:namespace) { namespaces.create!(name: 'ns', path: 'ns') }
|
||||
let(:project) { projects.create!(namespace_id: namespace.id) }
|
||||
|
||||
let!(:issue1) do
|
||||
issues.create!(
|
||||
title: 'issue',
|
||||
description: 'description',
|
||||
project_id: project.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:issue2) do
|
||||
issues.create!(
|
||||
title: 'issue',
|
||||
description: 'description',
|
||||
project_id: project.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:merge_request1) do
|
||||
merge_requests.create!(
|
||||
source_branch: 'a',
|
||||
target_branch: 'master',
|
||||
target_project_id: project.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:merge_request2) do
|
||||
merge_requests.create!(
|
||||
source_branch: 'b',
|
||||
target_branch: 'master',
|
||||
target_project_id: project.id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:merge_request_closing_issue1) do
|
||||
merge_requests_closing_issues.create!(issue_id: issue1.id, merge_request_id: merge_request1.id)
|
||||
end
|
||||
|
||||
let!(:merge_request_closing_issue2) do
|
||||
merge_requests_closing_issues.create!(issue_id: issue2.id, merge_request_id: merge_request2.id)
|
||||
end
|
||||
|
||||
let!(:diff1) { diffs.create!(merge_request_id: merge_request1.id) }
|
||||
let!(:diff2) { diffs.create!(merge_request_id: merge_request1.id) }
|
||||
|
||||
let!(:other_diff) { diffs.create!(merge_request_id: merge_request2.id) }
|
||||
|
||||
let!(:commit1) do
|
||||
commits.create!(
|
||||
merge_request_diff_id: diff2.id,
|
||||
relative_order: 0,
|
||||
sha: Gitlab::Database::ShaAttribute.serialize('aaa'),
|
||||
authored_date: 5.days.ago
|
||||
)
|
||||
end
|
||||
|
||||
let!(:commit2) do
|
||||
commits.create!(
|
||||
merge_request_diff_id: diff2.id,
|
||||
relative_order: 1,
|
||||
sha: Gitlab::Database::ShaAttribute.serialize('aaa'),
|
||||
authored_date: 10.days.ago
|
||||
)
|
||||
end
|
||||
|
||||
let!(:commit3) do
|
||||
commits.create!(
|
||||
merge_request_diff_id: other_diff.id,
|
||||
relative_order: 1,
|
||||
sha: Gitlab::Database::ShaAttribute.serialize('aaa'),
|
||||
authored_date: 5.days.ago
|
||||
)
|
||||
end
|
||||
|
||||
def run_migration
|
||||
described_class
|
||||
.new
|
||||
.perform(issue_metrics.minimum(:issue_id), issue_metrics.maximum(:issue_id))
|
||||
end
|
||||
|
||||
context 'when the persisted first_mentioned_in_commit_at is later than the first commit authored_date' do
|
||||
it 'updates the issue_metrics record' do
|
||||
record1 = issue_metrics.create!(issue_id: issue1.id, first_mentioned_in_commit_at: Time.current)
|
||||
record2 = issue_metrics.create!(issue_id: issue2.id, first_mentioned_in_commit_at: Time.current)
|
||||
|
||||
run_migration
|
||||
record1.reload
|
||||
record2.reload
|
||||
|
||||
expect(record1.first_mentioned_in_commit_at).to be_within(2.seconds).of(commit2.authored_date)
|
||||
expect(record2.first_mentioned_in_commit_at).to be_within(2.seconds).of(commit3.authored_date)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the persisted first_mentioned_in_commit_at is earlier than the first commit authored_date' do
|
||||
it 'does not update the issue_metrics record' do
|
||||
record = issue_metrics.create!(issue_id: issue1.id, first_mentioned_in_commit_at: 20.days.ago)
|
||||
|
||||
expect { run_migration }.not_to change { record.reload.first_mentioned_in_commit_at }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the first_mentioned_in_commit_at is null' do
|
||||
it 'does nothing' do
|
||||
record = issue_metrics.create!(issue_id: issue1.id, first_mentioned_in_commit_at: nil)
|
||||
|
||||
expect { run_migration }.not_to change { record.reload.first_mentioned_in_commit_at }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -308,4 +308,15 @@ RSpec.describe ProtectedBranch do
|
|||
expect(described_class.by_name('')).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '.get_ids_by_name' do
|
||||
let(:branch_name) { 'branch_name' }
|
||||
let!(:protected_branch) { create(:protected_branch, name: branch_name) }
|
||||
let(:branch_id) { protected_branch.id }
|
||||
|
||||
it 'returns the id for each protected branch matching name' do
|
||||
expect(described_class.get_ids_by_name([branch_name]))
|
||||
.to match_array([branch_id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6180,4 +6180,14 @@ RSpec.describe User do
|
|||
it_behaves_like 'groups_with_developer_maintainer_project_access examples'
|
||||
end
|
||||
end
|
||||
|
||||
describe '.get_ids_by_username' do
|
||||
let(:user_name) { 'user_name' }
|
||||
let!(:user) { create(:user, username: user_name) }
|
||||
let(:user_id) { user.id }
|
||||
|
||||
it 'returns the id of each record matching username' do
|
||||
expect(described_class.get_ids_by_username([user_name])).to match_array([user_id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,8 +24,8 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do
|
|||
|
||||
it 'completely clean up the repository' do
|
||||
expect(Projects::ContainerRepository::CleanupTagsService)
|
||||
.to receive(:new).with(project, nil, cleanup_tags_service_params).and_return(cleanup_tags_service)
|
||||
expect(cleanup_tags_service).to receive(:execute).with(repository).and_return(status: :success)
|
||||
.to receive(:new).with(repository, nil, cleanup_tags_service_params).and_return(cleanup_tags_service)
|
||||
expect(cleanup_tags_service).to receive(:execute).and_return(status: :success)
|
||||
|
||||
response = subject
|
||||
|
||||
|
|
|
@ -168,7 +168,7 @@ RSpec.describe Issues::CloseService do
|
|||
context 'updating `metrics.first_mentioned_in_commit_at`' do
|
||||
context 'when `metrics.first_mentioned_in_commit_at` is not set' do
|
||||
it 'uses the first commit authored timestamp' do
|
||||
expected = closing_merge_request.commits.first.authored_date
|
||||
expected = closing_merge_request.commits.take(100).last.authored_date
|
||||
|
||||
close_issue
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
|
|||
let_it_be(:project, reload: true) { create(:project, :private) }
|
||||
|
||||
let(:repository) { create(:container_repository, :root, project: project) }
|
||||
let(:service) { described_class.new(project, user, params) }
|
||||
let(:service) { described_class.new(repository, user, params) }
|
||||
let(:tags) { %w[latest A Ba Bb C D E] }
|
||||
|
||||
before do
|
||||
|
@ -39,7 +39,7 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
|
|||
end
|
||||
|
||||
describe '#execute' do
|
||||
subject { service.execute(repository) }
|
||||
subject { service.execute }
|
||||
|
||||
shared_examples 'reading and removing tags' do |caching_enabled: true|
|
||||
context 'when no params are specified' do
|
||||
|
|
|
@ -5,66 +5,24 @@ require 'spec_helper'
|
|||
RSpec.describe Ci::StuckBuilds::DropRunningWorker do
|
||||
include ExclusiveLeaseHelpers
|
||||
|
||||
let(:worker_lease_key) { Ci::StuckBuilds::DropRunningWorker::EXCLUSIVE_LEASE_KEY }
|
||||
let(:worker_lease_uuid) { SecureRandom.uuid }
|
||||
let(:worker2) { described_class.new }
|
||||
|
||||
subject(:worker) { described_class.new }
|
||||
|
||||
before do
|
||||
stub_exclusive_lease(worker_lease_key, worker_lease_uuid)
|
||||
end
|
||||
let(:worker) { described_class.new }
|
||||
let(:lease_uuid) { SecureRandom.uuid }
|
||||
|
||||
describe '#perform' do
|
||||
subject { worker.perform }
|
||||
|
||||
it_behaves_like 'an idempotent worker'
|
||||
|
||||
it 'executes an instance of Ci::StuckBuilds::DropRunningService' do
|
||||
expect_to_obtain_exclusive_lease(worker.lease_key, lease_uuid)
|
||||
|
||||
expect_next_instance_of(Ci::StuckBuilds::DropRunningService) do |service|
|
||||
expect(service).to receive(:execute).exactly(:once)
|
||||
end
|
||||
|
||||
worker.perform
|
||||
end
|
||||
expect_to_cancel_exclusive_lease(worker.lease_key, lease_uuid)
|
||||
|
||||
context 'with an exclusive lease' do
|
||||
it 'does not execute concurrently' do
|
||||
expect(worker).to receive(:remove_lease).exactly(:once)
|
||||
expect(worker2).not_to receive(:remove_lease)
|
||||
|
||||
worker.perform
|
||||
|
||||
stub_exclusive_lease_taken(worker_lease_key)
|
||||
|
||||
worker2.perform
|
||||
end
|
||||
|
||||
it 'can execute in sequence' do
|
||||
expect(worker).to receive(:remove_lease).at_least(:once)
|
||||
expect(worker2).to receive(:remove_lease).at_least(:once)
|
||||
|
||||
worker.perform
|
||||
worker2.perform
|
||||
end
|
||||
|
||||
it 'cancels exclusive leases after worker perform' do
|
||||
expect_to_cancel_exclusive_lease(worker_lease_key, worker_lease_uuid)
|
||||
|
||||
worker.perform
|
||||
end
|
||||
|
||||
context 'when the DropRunningService fails' do
|
||||
it 'ensures cancellation of the exclusive lease' do
|
||||
expect_to_cancel_exclusive_lease(worker_lease_key, worker_lease_uuid)
|
||||
|
||||
allow_next_instance_of(Ci::StuckBuilds::DropRunningService) do |service|
|
||||
expect(service).to receive(:execute) do
|
||||
raise 'The query timed out'
|
||||
end
|
||||
end
|
||||
|
||||
expect { worker.perform }.to raise_error(/The query timed out/)
|
||||
end
|
||||
end
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,7 +17,7 @@ RSpec.describe CleanupContainerRepositoryWorker, :clean_gitlab_redis_shared_stat
|
|||
|
||||
it 'executes the destroy service' do
|
||||
expect(Projects::ContainerRepository::CleanupTagsService).to receive(:new)
|
||||
.with(project, user, params.merge('container_expiration_policy' => false))
|
||||
.with(repository, user, params.merge('container_expiration_policy' => false))
|
||||
.and_return(service)
|
||||
expect(service).to receive(:execute)
|
||||
|
||||
|
@ -49,7 +49,7 @@ RSpec.describe CleanupContainerRepositoryWorker, :clean_gitlab_redis_shared_stat
|
|||
expect(repository).to receive(:start_expiration_policy!).and_call_original
|
||||
expect(repository).to receive(:reset_expiration_policy_started_at!).and_call_original
|
||||
expect(Projects::ContainerRepository::CleanupTagsService).to receive(:new)
|
||||
.with(project, nil, params.merge('container_expiration_policy' => true))
|
||||
.with(repository, nil, params.merge('container_expiration_policy' => true))
|
||||
.and_return(service)
|
||||
|
||||
expect(service).to receive(:execute).and_return(status: :success)
|
||||
|
@ -62,7 +62,7 @@ RSpec.describe CleanupContainerRepositoryWorker, :clean_gitlab_redis_shared_stat
|
|||
expect(repository).to receive(:start_expiration_policy!).and_call_original
|
||||
expect(repository).not_to receive(:reset_expiration_policy_started_at!)
|
||||
expect(Projects::ContainerRepository::CleanupTagsService).to receive(:new)
|
||||
.with(project, nil, params.merge('container_expiration_policy' => true))
|
||||
.with(repository, nil, params.merge('container_expiration_policy' => true))
|
||||
.and_return(service)
|
||||
|
||||
expect(service).to receive(:execute).and_return(status: :error, message: 'timeout while deleting tags')
|
||||
|
|
|
@ -5,76 +5,34 @@ require 'spec_helper'
|
|||
RSpec.describe StuckCiJobsWorker do
|
||||
include ExclusiveLeaseHelpers
|
||||
|
||||
let(:worker_lease_key) { StuckCiJobsWorker::EXCLUSIVE_LEASE_KEY }
|
||||
let(:worker_lease_uuid) { SecureRandom.uuid }
|
||||
let(:worker2) { described_class.new }
|
||||
|
||||
subject(:worker) { described_class.new }
|
||||
|
||||
before do
|
||||
stub_exclusive_lease(worker_lease_key, worker_lease_uuid)
|
||||
end
|
||||
let(:worker) { described_class.new }
|
||||
let(:lease_uuid) { SecureRandom.uuid }
|
||||
|
||||
describe '#perform' do
|
||||
subject { worker.perform }
|
||||
|
||||
it 'enqueues a Ci::StuckBuilds::DropRunningWorker job' do
|
||||
expect(Ci::StuckBuilds::DropRunningWorker).to receive(:perform_in).with(20.minutes).exactly(:once)
|
||||
|
||||
worker.perform
|
||||
subject
|
||||
end
|
||||
|
||||
it 'enqueues a Ci::StuckBuilds::DropScheduledWorker job' do
|
||||
expect(Ci::StuckBuilds::DropScheduledWorker).to receive(:perform_in).with(40.minutes).exactly(:once)
|
||||
|
||||
worker.perform
|
||||
subject
|
||||
end
|
||||
|
||||
it 'executes an instance of Ci::StuckBuilds::DropService' do
|
||||
expect_to_obtain_exclusive_lease(worker.lease_key, lease_uuid)
|
||||
|
||||
expect_next_instance_of(Ci::StuckBuilds::DropService) do |service|
|
||||
expect(service).to receive(:execute).exactly(:once)
|
||||
end
|
||||
|
||||
worker.perform
|
||||
end
|
||||
expect_to_cancel_exclusive_lease(worker.lease_key, lease_uuid)
|
||||
|
||||
context 'with an exclusive lease' do
|
||||
it 'does not execute concurrently' do
|
||||
expect(worker).to receive(:remove_lease).exactly(:once)
|
||||
expect(worker2).not_to receive(:remove_lease)
|
||||
|
||||
worker.perform
|
||||
|
||||
stub_exclusive_lease_taken(worker_lease_key)
|
||||
|
||||
worker2.perform
|
||||
end
|
||||
|
||||
it 'can execute in sequence' do
|
||||
expect(worker).to receive(:remove_lease).at_least(:once)
|
||||
expect(worker2).to receive(:remove_lease).at_least(:once)
|
||||
|
||||
worker.perform
|
||||
worker2.perform
|
||||
end
|
||||
|
||||
it 'cancels exclusive leases after worker perform' do
|
||||
expect_to_cancel_exclusive_lease(worker_lease_key, worker_lease_uuid)
|
||||
|
||||
worker.perform
|
||||
end
|
||||
|
||||
context 'when the DropService fails' do
|
||||
it 'ensures cancellation of the exclusive lease' do
|
||||
expect_to_cancel_exclusive_lease(worker_lease_key, worker_lease_uuid)
|
||||
|
||||
allow_next_instance_of(Ci::StuckBuilds::DropService) do |service|
|
||||
expect(service).to receive(:execute) do
|
||||
raise 'The query timed out'
|
||||
end
|
||||
end
|
||||
|
||||
expect { worker.perform }.to raise_error(/The query timed out/)
|
||||
end
|
||||
end
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue