Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-07-14 12:09:23 +00:00
parent c1e7698dff
commit 0b194c4854
85 changed files with 1312 additions and 226 deletions

View file

@ -295,7 +295,7 @@ gem 'gon', '~> 6.4.0'
gem 'request_store', '~> 1.5'
gem 'base32', '~> 0.3.0'
gem 'gitlab-license', '~> 1.5'
gem 'gitlab-license', '~> 2.0'
# Protect against bruteforcing
gem 'rack-attack', '~> 6.3.0'

View file

@ -490,7 +490,7 @@ GEM
opentracing (~> 0.4)
pg_query (~> 2.1)
redis (> 3.0.0, < 5.0.0)
gitlab-license (1.5.0)
gitlab-license (2.0.0)
gitlab-mail_room (0.0.9)
gitlab-markup (1.7.1)
gitlab-net-dns (0.9.1)
@ -1489,7 +1489,7 @@ DEPENDENCIES
gitlab-experiment (~> 0.6.1)
gitlab-fog-azure-rm (~> 1.1.1)
gitlab-labkit (~> 0.20.0)
gitlab-license (~> 1.5)
gitlab-license (~> 2.0)
gitlab-mail_room (~> 0.0.9)
gitlab-markup (~> 1.7.1)
gitlab-net-dns (~> 0.9.1)

View file

@ -227,7 +227,12 @@ export default {
</template>
</gl-sprintf>
</span>
<gl-search-box-by-click class="gl-ml-auto" @submit="filter = $event" @clear="filter = ''" />
<gl-search-box-by-click
class="gl-ml-auto"
:placeholder="s__('BulkImport|Filter by source group')"
@submit="filter = $event"
@clear="filter = ''"
/>
</div>
<gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" />
<template v-else>

View file

@ -225,11 +225,21 @@ export default {
{
name: 'success',
data: this.mergeLabelsAndValues(labels, success),
areaStyle: {
color: this.$options.successColor,
},
lineStyle: {
color: this.$options.successColor,
},
itemStyle: {
color: this.$options.successColor,
},
},
],
};
},
},
successColor: '#608b2f',
chartContainerHeight: CHART_CONTAINER_HEIGHT,
timesChartOptions: {
height: INNER_CHART_HEIGHT,

View file

@ -3,6 +3,7 @@ import { GlButtonGroup, GlButton, GlModalDirective } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { sprintf, __ } from '~/locale';
import getRefMixin from '../mixins/get_ref';
import DeleteBlobModal from './delete_blob_modal.vue';
import UploadBlobModal from './upload_blob_modal.vue';
export default {
@ -15,6 +16,7 @@ export default {
GlButtonGroup,
GlButton,
UploadBlobModal,
DeleteBlobModal,
},
directives: {
GlModal: GlModalDirective,
@ -41,10 +43,18 @@ export default {
type: String,
required: true,
},
deletePath: {
type: String,
required: true,
},
canPushCode: {
type: Boolean,
required: true,
},
emptyRepo: {
type: Boolean,
required: true,
},
},
computed: {
replaceModalId() {
@ -53,6 +63,12 @@ export default {
replaceModalTitle() {
return sprintf(__('Replace %{name}'), { name: this.name });
},
deleteModalId() {
return uniqueId('delete-modal');
},
deleteModalTitle() {
return sprintf(__('Delete %{name}'), { name: this.name });
},
},
};
</script>
@ -63,7 +79,9 @@ export default {
<gl-button v-gl-modal="replaceModalId">
{{ $options.i18n.replace }}
</gl-button>
<gl-button>{{ $options.i18n.delete }}</gl-button>
<gl-button v-gl-modal="deleteModalId">
{{ $options.i18n.delete }}
</gl-button>
</gl-button-group>
<upload-blob-modal
:modal-id="replaceModalId"
@ -76,5 +94,15 @@ export default {
:replace-path="replacePath"
:primary-btn-text="$options.i18n.replacePrimaryBtnText"
/>
<delete-blob-modal
:modal-id="deleteModalId"
:modal-title="deleteModalTitle"
:delete-path="deletePath"
:commit-message="deleteModalTitle"
:target-branch="targetBranch || ref"
:original-branch="originalBranch || ref"
:can-push-code="canPushCode"
:empty-repo="emptyRepo"
/>
</div>
</template>

View file

@ -69,6 +69,7 @@ export default {
pushCode: false,
},
repository: {
empty: true,
blobs: {
nodes: [
{
@ -92,6 +93,7 @@ export default {
forkPath: '',
simpleViewer: {},
richViewer: null,
webPath: '',
},
],
},
@ -174,7 +176,9 @@ export default {
:path="path"
:name="blobInfo.name"
:replace-path="blobInfo.replacePath"
:delete-path="blobInfo.webPath"
:can-push-code="project.userPermissions.pushCode"
:empty-repo="project.repository.empty"
/>
</template>
</blob-header>

View file

@ -0,0 +1,151 @@
<script>
import { GlModal, GlFormGroup, GlFormInput, GlFormTextarea, GlToggle } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __ } from '~/locale';
import {
SECONDARY_OPTIONS_TEXT,
COMMIT_LABEL,
TARGET_BRANCH_LABEL,
TOGGLE_CREATE_MR_LABEL,
} from '../constants';
export default {
csrf,
components: {
GlModal,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlToggle,
},
i18n: {
PRIMARY_OPTIONS_TEXT: __('Delete file'),
SECONDARY_OPTIONS_TEXT,
COMMIT_LABEL,
TARGET_BRANCH_LABEL,
TOGGLE_CREATE_MR_LABEL,
},
props: {
modalId: {
type: String,
required: true,
},
modalTitle: {
type: String,
required: true,
},
deletePath: {
type: String,
required: true,
},
commitMessage: {
type: String,
required: true,
},
targetBranch: {
type: String,
required: true,
},
originalBranch: {
type: String,
required: true,
},
canPushCode: {
type: Boolean,
required: true,
},
emptyRepo: {
type: Boolean,
required: true,
},
},
data() {
return {
loading: false,
commit: this.commitMessage,
target: this.targetBranch,
createNewMr: true,
error: '',
};
},
computed: {
primaryOptions() {
return {
text: this.$options.i18n.PRIMARY_OPTIONS_TEXT,
attributes: [
{
variant: 'danger',
loading: this.loading,
disabled: !this.formCompleted || this.loading,
},
],
};
},
cancelOptions() {
return {
text: this.$options.i18n.SECONDARY_OPTIONS_TEXT,
attributes: [
{
disabled: this.loading,
},
],
};
},
showCreateNewMrToggle() {
return this.canPushCode && this.target !== this.originalBranch;
},
formCompleted() {
return this.commit && this.target;
},
},
methods: {
submitForm(e) {
e.preventDefault(); // Prevent modal from closing
this.loading = true;
this.$refs.form.submit();
},
},
};
</script>
<template>
<gl-modal
:modal-id="modalId"
:title="modalTitle"
:action-primary="primaryOptions"
:action-cancel="cancelOptions"
@primary="submitForm"
>
<form ref="form" :action="deletePath" method="post">
<input type="hidden" name="_method" value="delete" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
<template v-if="emptyRepo">
<!-- Once "empty_repo_upload_experiment" is made available, will need to add class 'js-branch-name'
Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/335721 -->
<input type="hidden" name="branch_name" :value="originalBranch" />
</template>
<template v-else>
<input type="hidden" name="original_branch" :value="originalBranch" />
<!-- Once "push to branch" permission is made available, will need to add to conditional
Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/335462 -->
<input v-if="createNewMr" type="hidden" name="create_merge_request" value="1" />
<gl-form-group :label="$options.i18n.COMMIT_LABEL" label-for="commit_message">
<gl-form-textarea v-model="commit" name="commit_message" :disabled="loading" />
</gl-form-group>
<gl-form-group
v-if="canPushCode"
:label="$options.i18n.TARGET_BRANCH_LABEL"
label-for="branch_name"
>
<gl-form-input v-model="target" :disabled="loading" name="branch_name" />
</gl-form-group>
<gl-toggle
v-if="showCreateNewMrToggle"
v-model="createNewMr"
:disabled="loading"
:label="$options.i18n.TOGGLE_CREATE_MR_LABEL"
/>
</template>
</form>
</gl-modal>
</template>

View file

@ -1,3 +1,10 @@
import { __ } from '~/locale';
export const TREE_PAGE_LIMIT = 1000; // the maximum amount of items per page
export const TREE_PAGE_SIZE = 100; // the amount of items to be fetched per (batch) request
export const TREE_INITIAL_FETCH_COUNT = TREE_PAGE_LIMIT / TREE_PAGE_SIZE; // the amount of (batch) requests to make
export const SECONDARY_OPTIONS_TEXT = __('Cancel');
export const COMMIT_LABEL = __('Commit message');
export const TARGET_BRANCH_LABEL = __('Target branch');
export const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these changes');

View file

@ -4,6 +4,7 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) {
pushCode
}
repository {
empty
blobs(paths: [$filePath]) {
nodes {
webPath

View file

@ -6,7 +6,7 @@
* @param {string} list[].getter - the name of the getter, leave it empty to not use a getter
* @param {string} list[].updateFn - the name of the action, leave it empty to use the default action
* @param {string} defaultUpdateFn - the default function to dispatch
* @param {string} root - the key of the state where to search fo they keys described in list
* @param {string|function} root - the key of the state where to search for the keys described in list
* @returns {Object} a dictionary with all the computed properties generated
*/
export const mapComputed = (list, defaultUpdateFn, root) => {
@ -21,6 +21,10 @@ export const mapComputed = (list, defaultUpdateFn, root) => {
if (getter) {
return this.$store.getters[getter];
} else if (root) {
if (typeof root === 'function') {
return root(this.$store.state)[key];
}
return this.$store.state[root][key];
}
return this.$store.state[key];

View file

@ -1673,7 +1673,7 @@ body.gl-dark .nav-sidebar .fly-out-top-item a,
body.gl-dark .nav-sidebar .fly-out-top-item.active a,
body.gl-dark .nav-sidebar .fly-out-top-item .fly-out-top-item-container {
background-color: #2f2a6b;
color: #333;
color: var(--black, #333);
}
body.gl-dark .logo-text svg {
fill: var(--gl-text-color);

View file

@ -185,7 +185,7 @@
&.active a,
.fly-out-top-item-container {
background-color: $purple-900;
color: $white;
color: var(--black, $white);
}
}
}

View file

@ -23,6 +23,10 @@ class Projects::BlobController < Projects::ApplicationController
# We need to assign the blob vars before `authorize_edit_tree!` so we can
# validate access to a specific ref.
before_action :assign_blob_vars
# Since BlobController doesn't use assign_ref_vars, we have to call this explicitly
before_action :rectify_renamed_default_branch!, only: [:show]
before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy]
before_action :commit, except: [:new, :create]
@ -140,11 +144,15 @@ class Projects::BlobController < Projects::ApplicationController
end
def commit
@commit = @repository.commit(@ref)
@commit ||= @repository.commit(@ref)
return render_404 unless @commit
end
def redirect_renamed_default_branch?
action_name == 'show'
end
def assign_blob_vars
@id = params[:id]
@ref, @path = extract_ref(@id)
@ -152,6 +160,12 @@ class Projects::BlobController < Projects::ApplicationController
render_404
end
def rectify_renamed_default_branch!
@commit ||= @repository.commit(@ref)
super
end
# rubocop: disable CodeReuse/ActiveRecord
def after_edit_path
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:from_merge_request_iid])

View file

@ -39,6 +39,10 @@ class Projects::TreeController < Projects::ApplicationController
private
def redirect_renamed_default_branch?
action_name == 'show'
end
def assign_dir_vars
@branch_name = params[:branch_name]

View file

@ -20,7 +20,11 @@ module ClustersHelper
{
default_branch_name: clusterable_project.default_branch,
empty_state_image: image_path('illustrations/clusters_empty.svg'),
project_path: clusterable_project.full_path
project_path: clusterable_project.full_path,
agent_docs_url: help_page_path('user/clusters/agent/index'),
install_docs_url: help_page_path('administration/clusters/kas'),
get_started_docs_url: help_page_path('user/clusters/agent/index', anchor: 'define-a-configuration-repository'),
integration_docs_url: help_page_path('user/clusters/agent/index', anchor: 'get-started-with-gitops-and-the-gitlab-agent')
}
end

View file

@ -131,7 +131,7 @@ module SearchHelper
end
def search_sort_options
[
options = [
{
title: _('Created date'),
sortable: true,
@ -149,6 +149,19 @@ module SearchHelper
}
}
]
if search_service.scope == 'issues' && Feature.enabled?(:search_sort_issues_by_popularity)
options << {
title: _('Popularity'),
sortable: true,
sortParam: {
asc: 'popularity_asc',
desc: 'popularity_desc'
}
}
end
options
end
private

View file

@ -27,9 +27,6 @@ class AwardEmoji < ApplicationRecord
after_save :expire_cache
after_destroy :expire_cache
after_save :update_awardable_upvotes_count
after_destroy :update_awardable_upvotes_count
class << self
def votes_for_collection(ids, type)
select('name', 'awardable_id', 'COUNT(*) as count')
@ -66,15 +63,6 @@ class AwardEmoji < ApplicationRecord
def expire_cache
awardable.try(:bump_updated_at)
awardable.try(:expire_etag_cache)
end
private
def update_awardable_upvotes_count
return unless upvote? && awardable.has_attribute?(:upvotes_count)
awardable.update_column(:upvotes_count, awardable.upvotes)
awardable.try(:update_upvotes_count) if upvote?
end
end
AwardEmoji.prepend_mod_with('AwardEmoji')

View file

@ -520,6 +520,11 @@ class Issue < ApplicationRecord
issue_assignees.pluck(:user_id)
end
def update_upvotes_count
self.lock!
self.update_column(:upvotes_count, self.upvotes)
end
private
def spammable_attribute_changed?

View file

@ -1,14 +1,61 @@
# frozen_string_literal: true
class MergeRequest::CleanupSchedule < ApplicationRecord
STATUSES = {
unstarted: 0,
running: 1,
completed: 2,
failed: 3
}.freeze
belongs_to :merge_request, inverse_of: :cleanup_schedule
validates :scheduled_at, presence: true
def self.scheduled_merge_request_ids(limit)
where('completed_at IS NULL AND scheduled_at <= NOW()')
state_machine :status, initial: :unstarted do
state :unstarted, value: STATUSES[:unstarted]
state :running, value: STATUSES[:running]
state :completed, value: STATUSES[:completed]
state :failed, value: STATUSES[:failed]
event :run do
transition unstarted: :running
end
event :retry do
transition running: :unstarted
end
event :complete do
transition running: :completed
end
event :mark_as_failed do
transition running: :failed
end
before_transition to: [:completed] do |cleanup_schedule, _transition|
cleanup_schedule.completed_at = Time.current
end
before_transition from: :running, to: [:unstarted, :failed] do |cleanup_schedule, _transition|
cleanup_schedule.failed_count += 1
end
end
scope :scheduled_and_unstarted, -> {
where('completed_at IS NULL AND scheduled_at <= NOW() AND status = ?', STATUSES[:unstarted])
.order('scheduled_at DESC')
.limit(limit)
.pluck(:merge_request_id)
}
def self.start_next
MergeRequest::CleanupSchedule.transaction do
cleanup_schedule = scheduled_and_unstarted.lock('FOR UPDATE SKIP LOCKED').first
next if cleanup_schedule.blank?
cleanup_schedule.run!
cleanup_schedule
end
end
end

View file

@ -416,6 +416,7 @@ class Project < ApplicationRecord
prefix: :import, to: :import_state, allow_nil: true
delegate :squash_always?, :squash_never?, :squash_enabled_by_default?, :squash_readonly?, to: :project_setting
delegate :squash_option, to: :project_setting
delegate :previous_default_branch, :previous_default_branch=, to: :project_setting
delegate :no_import?, to: :import_state, allow_nil: true
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true

View file

@ -66,6 +66,8 @@ module Projects
previous_default_branch = project.default_branch
if project.change_head(params[:default_branch])
params[:previous_default_branch] = previous_default_branch
after_default_branch_change(previous_default_branch)
else
raise ValidationError, s_("UpdateProject|Could not set the default branch")

View file

@ -31,8 +31,8 @@
.js-text.d-inline= _('Preview payload')
%pre.service-data-payload-container.js-syntax-highlight.code.highlight.mt-2.d-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
- else
= _('Service ping is disabled, and cannot be configured through this form.')
- deactivating_service_ping_path = help_page_path('development/service_ping/index.md', anchor: 'disable-service-ping')
= _('Service ping is disabled in your configuration file, and cannot be enabled through this form.')
- deactivating_service_ping_path = help_page_path('development/service_ping/index.md', anchor: 'disable-service-ping-using-the-configuration-file')
- deactivating_service_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_service_ping_path }
= s_('For more information, see the documentation on %{deactivating_service_ping_link_start}deactivating service ping%{deactivating_service_ping_link_end}.').html_safe % { deactivating_service_ping_link_start: deactivating_service_ping_link_start, deactivating_service_ping_link_end: '</a>'.html_safe }
.form-group

View file

@ -8,6 +8,6 @@
%h4
= _('Introducing Your DevOps Report')
%p
= _('Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. View how you compare with other organizations, discover features you are not using, and learn best practices through blog posts and white papers.')
= _('Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. Use it to view how you compare with other organizations.')
.svg-container.devops
= custom_icon('dev_ops_report_overview')

View file

@ -6,6 +6,7 @@
.js-invite-members-trigger{ data: { variant: 'confirm',
classes: 'gl-mb-8 gl-xs-w-full',
display_text: s_('InviteMember|Invite members'),
trigger_source: 'project-empty-page',
event: 'click_button',
label: 'invite_members_empty_project' } }

View file

@ -7,7 +7,7 @@
= render "home_panel"
= render "archived_notice", project: @project
= render "invite_members" if can_import_members?
= render 'invite_members_empty_project' if can_import_members?
%h4.gl-mt-0.gl-mb-3
= _('The repository for this project is empty')

View file

@ -1,14 +1,19 @@
%div{ class: 'search-result-row gl-pb-3! gl-mt-5 gl-mb-0!' }
%span.gl-display-flex.gl-align-items-center
%span.badge.badge-pill.gl-badge.sm{ class: "badge-#{issuable_state_to_badge_class(issuable)}" }= issuable_state_text(issuable)
= sprite_icon('eye-slash', css_class: 'gl-text-gray-500 gl-ml-2') if issuable.respond_to?(:confidential?) && issuable.confidential?
= link_to issuable_path(issuable), data: { track_event: 'click_text', track_label: "#{issuable.class.name.downcase}_title", track_property: 'search_result' }, class: 'gl-w-full' do
%span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issuable.title
.gl-text-gray-500.gl-my-3
= issuable_project_reference(issuable)
&middot;
= sprintf(s_('created %{issuable_created} by %{author}'), { issuable_created: time_ago_with_tooltip(issuable.created_at, placement: 'bottom'), author: link_to_member(@project, issuable.author, avatar: false) }).html_safe
&middot;
= sprintf(s_('updated %{time_ago}'), { time_ago: time_ago_with_tooltip(issuable.updated_at, placement: 'bottom') }).html_safe
.description.term.col-sm-10.gl-px-0
= highlight_and_truncate_issuable(issuable, @search_term, @search_highlight)
%div{ class: 'search-result-row gl-display-flex gl-sm-flex-direction-row gl-flex-direction-column gl-align-items-center gl-pb-3! gl-mt-5 gl-mb-0!' }
.col-sm-9
%span.gl-display-flex.gl-align-items-center
%span.badge.badge-pill.gl-badge.sm{ class: "badge-#{issuable_state_to_badge_class(issuable)}" }= issuable_state_text(issuable)
= sprite_icon('eye-slash', css_class: 'gl-text-gray-500 gl-ml-2') if issuable.respond_to?(:confidential?) && issuable.confidential?
= link_to issuable_path(issuable), data: { track_event: 'click_text', track_label: "#{issuable.class.name.downcase}_title", track_property: 'search_result' }, class: 'gl-w-full' do
%span.term.str-truncated.gl-font-weight-bold.gl-ml-2= issuable.title
.gl-text-gray-500.gl-my-3
= issuable_project_reference(issuable)
&middot;
= sprintf(s_('created %{issuable_created} by %{author}'), { issuable_created: time_ago_with_tooltip(issuable.created_at, placement: 'bottom'), author: link_to_member(@project, issuable.author, avatar: false) }).html_safe
.description.term.gl-px-0
= highlight_and_truncate_issuable(issuable, @search_term, @search_highlight)
.col-sm-3.gl-mt-3.gl-sm-mt-0.gl-text-right
- if Feature.enabled?(:search_sort_issues_by_popularity) && issuable.respond_to?(:upvotes_count) && issuable.upvotes_count > 0
%li.issuable-upvotes.gl-list-style-none.has-tooltip{ title: _('Upvotes') }
= sprite_icon('thumb-up', css_class: "gl-vertical-align-middle")
= issuable.upvotes_count
%span.gl-text-gray-500= sprintf(s_('updated %{time_ago}'), { time_ago: time_ago_with_tooltip(issuable.updated_at, placement: 'bottom') }).html_safe

View file

@ -2,6 +2,8 @@
class MergeRequestCleanupRefsWorker
include ApplicationWorker
include LimitedCapacity::Worker
include Gitlab::Utils::StrongMemoize
sidekiq_options retry: 3
@ -9,20 +11,60 @@ class MergeRequestCleanupRefsWorker
tags :exclude_from_kubernetes
idempotent!
def perform(merge_request_id)
# Hard-coded to 4 for now. Will be configurable later on via application settings.
# This means, there can only be 4 jobs running at the same time at maximum.
MAX_RUNNING_JOBS = 4
FAILURE_THRESHOLD = 3
def perform_work
return unless Feature.enabled?(:merge_request_refs_cleanup, default_enabled: false)
merge_request = MergeRequest.find_by_id(merge_request_id)
unless merge_request
logger.error("Failed to find merge request with ID: #{merge_request_id}")
logger.error('No existing merge request to be cleaned up.')
return
end
result = ::MergeRequests::CleanupRefsService.new(merge_request).execute
log_extra_metadata_on_done(:merge_request_id, merge_request.id)
return if result[:status] == :success
result = MergeRequests::CleanupRefsService.new(merge_request).execute
logger.error("Failed cleanup refs of merge request (#{merge_request_id}): #{result[:message]}")
if result[:status] == :success
merge_request_cleanup_schedule.complete!
else
if merge_request_cleanup_schedule.failed_count < FAILURE_THRESHOLD
merge_request_cleanup_schedule.retry!
else
merge_request_cleanup_schedule.mark_as_failed!
end
log_extra_metadata_on_done(:message, result[:message])
end
log_extra_metadata_on_done(:status, merge_request_cleanup_schedule.status)
end
def remaining_work_count
MergeRequest::CleanupSchedule
.scheduled_and_unstarted
.limit(max_running_jobs)
.count
end
def max_running_jobs
MAX_RUNNING_JOBS
end
private
def merge_request
strong_memoize(:merge_request) do
merge_request_cleanup_schedule&.merge_request
end
end
def merge_request_cleanup_schedule
strong_memoize(:merge_request_cleanup_schedule) do
MergeRequest::CleanupSchedule.start_next
end
end
end

View file

@ -10,21 +10,10 @@ class ScheduleMergeRequestCleanupRefsWorker
tags :exclude_from_kubernetes
idempotent!
# Based on existing data, MergeRequestCleanupRefsWorker can run 3 jobs per
# second. This means that 180 jobs can be performed but since there are some
# spikes from time time, it's better to give it some allowance.
LIMIT = 180
DELAY = 10.seconds
BATCH_SIZE = 30
def perform
return if Gitlab::Database.read_only?
return unless Feature.enabled?(:merge_request_refs_cleanup, default_enabled: false)
ids = MergeRequest::CleanupSchedule.scheduled_merge_request_ids(LIMIT).map { |id| [id] }
MergeRequestCleanupRefsWorker.bulk_perform_in(DELAY, ids, batch_size: BATCH_SIZE) # rubocop:disable Scalability/BulkPerformWithContext
log_extra_metadata_on_done(:merge_requests_count, ids.size)
MergeRequestCleanupRefsWorker.perform_with_capacity
end
end

View file

@ -1,7 +1,7 @@
---
name: merge_request_discussion_cache
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64688
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332967
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/335799
milestone: '14.1'
type: development
group: group::code review

View file

@ -1,7 +1,7 @@
---
name: merge_request_refs_cleanup
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51558
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/296874
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336070
milestone: '13.8'
type: development
group: group::code review

View file

@ -0,0 +1,8 @@
---
name: search_sort_issues_by_popularity
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65231
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334974
milestone: '14.1'
type: development
group: group::global search
default_enabled: false

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
class AddProjectSettingsPreviousDefaultBranch < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
# rubocop:disable Migration/AddLimitToTextColumns
# limit is added in 20210707173645_add_project_settings_previous_default_branch_text_limit
def up
with_lock_retries do
add_column :project_settings, :previous_default_branch, :text
end
end
# rubocop:enable Migration/AddLimitToTextColumns
def down
with_lock_retries do
remove_column :project_settings, :previous_default_branch
end
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
class AddStatusToMergeRequestCleanupSchedules < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
INDEX_NAME = 'index_merge_request_cleanup_schedules_on_status'
disable_ddl_transaction!
def up
unless column_exists?(:merge_request_cleanup_schedules, :status)
add_column(:merge_request_cleanup_schedules, :status, :integer, limit: 2, default: 0, null: false)
end
add_concurrent_index(:merge_request_cleanup_schedules, :status, name: INDEX_NAME)
end
def down
remove_concurrent_index_by_name(:merge_request_cleanup_schedules, INDEX_NAME)
if column_exists?(:merge_request_cleanup_schedules, :status)
remove_column(:merge_request_cleanup_schedules, :status)
end
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddProjectSettingsPreviousDefaultBranchTextLimit < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
add_text_limit :project_settings, :previous_default_branch, 4096
end
def down
remove_text_limit :project_settings, :previous_default_branch
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddFailedCountToMergeRequestCleanupSchedules < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
def change
add_column :merge_request_cleanup_schedules, :failed_count, :integer, default: 0, null: false
end
end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
class UpdateMergeRequestCleanupSchedulesScheduledAtIndex < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
INDEX_NAME = 'index_mr_cleanup_schedules_timestamps_status'
OLD_INDEX_NAME = 'index_mr_cleanup_schedules_timestamps'
disable_ddl_transaction!
def up
add_concurrent_index(:merge_request_cleanup_schedules, :scheduled_at, where: 'completed_at IS NULL AND status = 0', name: INDEX_NAME)
remove_concurrent_index_by_name(:merge_request_cleanup_schedules, OLD_INDEX_NAME)
end
def down
remove_concurrent_index_by_name(:merge_request_cleanup_schedules, INDEX_NAME)
add_concurrent_index(:merge_request_cleanup_schedules, :scheduled_at, where: 'completed_at IS NULL', name: OLD_INDEX_NAME)
end
end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddUpvotesCountIndexToIssues < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
INDEX_NAME = 'index_issues_on_project_id_and_upvotes_count'
def up
add_concurrent_index :issues, [:project_id, :upvotes_count], name: INDEX_NAME
end
def down
remove_concurrent_index :issues, [:project_id, :upvotes_count], name: INDEX_NAME
end
end

View file

@ -0,0 +1 @@
02aea8fe759614bc3aa751e023aa508963f8183366f6d6f518bbccc2d85ec1a1

View file

@ -0,0 +1 @@
ac150e706b115849aa3802ae7b8e07d983e89eb637c48582c64948cbc7d7163d

View file

@ -0,0 +1 @@
98d4deaf0564119c1ee44d76d3a30bff1a0fceb7cab67c5dbef576faef62ddf5

View file

@ -0,0 +1 @@
e440dac0e14df7309c84e72b98ed6373c712901dc66310a474979e0fce7dc59c

View file

@ -0,0 +1 @@
77f6db1d2aeebdefd76c96966da6c9e4ce5da2c92a42f6ac2398b35fa21c680f

View file

@ -0,0 +1 @@
2899d954a199fa52bf6ab4beca5f22dcb9f9f0312e658f1307d1a7355394f1bb

View file

@ -14711,7 +14711,9 @@ CREATE TABLE merge_request_cleanup_schedules (
scheduled_at timestamp with time zone NOT NULL,
completed_at timestamp with time zone,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL
updated_at timestamp with time zone NOT NULL,
status smallint DEFAULT 0 NOT NULL,
failed_count integer DEFAULT 0 NOT NULL
);
CREATE SEQUENCE merge_request_cleanup_schedules_merge_request_id_seq
@ -17082,6 +17084,8 @@ CREATE TABLE project_settings (
prevent_merge_without_jira_issue boolean DEFAULT false NOT NULL,
cve_id_request_enabled boolean DEFAULT true NOT NULL,
mr_default_target_self boolean DEFAULT false NOT NULL,
previous_default_branch text,
CONSTRAINT check_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)),
CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL))
);
@ -23866,6 +23870,8 @@ CREATE UNIQUE INDEX index_issues_on_project_id_and_external_key ON issues USING
CREATE UNIQUE INDEX index_issues_on_project_id_and_iid ON issues USING btree (project_id, iid);
CREATE INDEX index_issues_on_project_id_and_upvotes_count ON issues USING btree (project_id, upvotes_count);
CREATE INDEX index_issues_on_promoted_to_epic_id ON issues USING btree (promoted_to_epic_id) WHERE (promoted_to_epic_id IS NOT NULL);
CREATE INDEX index_issues_on_sprint_id ON issues USING btree (sprint_id);
@ -23988,6 +23994,8 @@ CREATE INDEX index_merge_request_blocks_on_blocked_merge_request_id ON merge_req
CREATE UNIQUE INDEX index_merge_request_cleanup_schedules_on_merge_request_id ON merge_request_cleanup_schedules USING btree (merge_request_id);
CREATE INDEX index_merge_request_cleanup_schedules_on_status ON merge_request_cleanup_schedules USING btree (status);
CREATE UNIQUE INDEX index_merge_request_diff_commit_users_on_name_and_email ON merge_request_diff_commit_users USING btree (name, email);
CREATE INDEX index_merge_request_diff_commits_on_sha ON merge_request_diff_commits USING btree (sha);
@ -24120,7 +24128,7 @@ CREATE INDEX index_mirror_data_non_scheduled_or_started ON project_mirror_data U
CREATE UNIQUE INDEX index_mr_blocks_on_blocking_and_blocked_mr_ids ON merge_request_blocks USING btree (blocking_merge_request_id, blocked_merge_request_id);
CREATE INDEX index_mr_cleanup_schedules_timestamps ON merge_request_cleanup_schedules USING btree (scheduled_at) WHERE (completed_at IS NULL);
CREATE INDEX index_mr_cleanup_schedules_timestamps_status ON merge_request_cleanup_schedules USING btree (scheduled_at) WHERE ((completed_at IS NULL) AND (status = 0));
CREATE UNIQUE INDEX index_mr_context_commits_on_merge_request_id_and_sha ON merge_request_context_commits USING btree (merge_request_id, sha);

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View file

@ -241,6 +241,26 @@ you can view a graph or download a CSV file with this data. From your project:
Code coverage data is also [available at the group level](../../user/group/repositories_analytics/index.md).
### Coverage check approval rule **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15765) in GitLab 14.0.
> - [Made configurable in Project Settings](https://gitlab.com/gitlab-org/gitlab/-/issues/331001) in GitLab 14.1.
You can implement merge request approvals to require approval by selected users or a group
when merging a merge request would cause the project's test coverage to decline.
Follow these steps to enable the `Coverage-Check` MR approval rule:
1. Go to your project and select **Settings > General**.
1. Expand **Merge request approvals**.
1. Select **Enable** next to the `Coverage-Check` approval rule.
1. Select the **Target branch**.
1. Set the number of **Approvals required** to greater than zero.
1. Select the users or groups to provide approval.
1. Select **Add approval rule**.
![Coverage-Check approval rule](img/coverage_check_approval_rule_14_1.png)
### Removing color codes
Some test coverage tools output with ANSI color codes that aren't

View file

@ -540,11 +540,11 @@ export default {
foo: ''
},
actions: {
updateBar() {...}
updateAll() {...}
updateBar() {...},
updateAll() {...},
},
getters: {
getFoo() {...}
getFoo() {...},
}
}
```
@ -559,13 +559,13 @@ export default {
* @param {string} list[].getter - the name of the getter, leave it empty to not use a getter
* @param {string} list[].updateFn - the name of the action, leave it empty to use the default action
* @param {string} defaultUpdateFn - the default function to dispatch
* @param {string} root - optional key of the state where to search fo they keys described in list
* @param {string|function} root - optional key of the state where to search for they keys described in list
* @returns {Object} a dictionary with all the computed properties generated
*/
...mapComputed(
[
'baz',
{ key: 'bar', updateFn: 'updateBar' }
{ key: 'bar', updateFn: 'updateBar' },
{ key: 'foo', getter: 'getFoo' },
],
'updateAll',
@ -575,3 +575,48 @@ export default {
```
`mapComputed` then generates the appropriate computed properties that get the data from the store and dispatch the correct action when updated.
In the event that the `root` of the key is more than one-level deep you can use a function to retrieve the relevant state object.
For instance, with a store like:
```javascript
// this store is non-functional and only used to give context to the example
export default {
state: {
foo: {
qux: {
baz: '',
bar: '',
foo: '',
},
},
},
actions: {
updateBar() {...},
updateAll() {...},
},
getters: {
getFoo() {...},
}
}
```
The `root` could be:
```javascript
import { mapComputed } from '~/vuex_shared/bindings'
export default {
computed: {
...mapComputed(
[
'baz',
{ key: 'bar', updateFn: 'updateBar' },
{ key: 'foo', getter: 'getFoo' },
],
'updateAll',
(state) => state.foo.qux,
),
}
}
```

View file

@ -4354,6 +4354,8 @@ The total count of Helm packages that have been published.
Group: `group::package`
Data Category: `Optional`
Status: `implemented`
Tiers: `free`, `premium`, `ultimate`

View file

@ -20,21 +20,22 @@ To see DevOps Report:
## DevOps Score
NOTE:
Your GitLab instance's [Service Ping](../settings/usage_statistics.md#service-ping) must be activated in order to use this feature.
To see the DevOps score, you must activate your GitLab instance's [Service Ping](../settings/usage_statistics.md#service-ping).
You can use the DevOps score to compare your DevOps status to other organizations.
The DevOps Score tab displays the usage of major GitLab features on your instance over
the last 30 days, averaged over the number of billable users in that time period. It also
provides a Lead score per feature, which is calculated based on GitLab analysis
of top-performing instances based on [Service Ping data](../settings/usage_statistics.md#service-ping) that GitLab has
collected. Your score is compared to the lead score of each feature and then expressed as a percentage at the bottom of said feature.
Your overall **DevOps Score** is an average of your feature scores. You can use this score to compare your DevOps status to other organizations.
The page also provides helpful links to articles and GitLab docs, to help you
improve your scores.
the last 30 days, averaged over the number of billable users in that time period.
You can also see the Leader usage score, calculated from top-performing instances based on
[Service Ping data](../settings/usage_statistics.md#service-ping) that GitLab has collected.
Your score is compared to the lead score of each feature and then expressed
as a percentage at the bottom of said feature. Your overall **DevOps Score** is an average of your
feature scores.
Service Ping data is aggregated on GitLab servers for analysis. Your usage
information is **not sent** to any other GitLab instances. If you have just started using GitLab, it may take a few weeks for data to be
collected before this feature is available.
information is **not sent** to any other GitLab instances.
If you have just started using GitLab, it might take a few weeks for data to be collected before this
feature is available.
## DevOps Adoption **(ULTIMATE SELF)**
@ -46,7 +47,7 @@ collected before this feature is available.
> - The Overview tab [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/330401) in GitLab 14.1.
> - DAST and SAST metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328033) in GitLab 14.1.
DevOps Adoption shows you which groups within your organization are using the most essential features of GitLab:
DevOps Adoption shows you which groups in your organization are using the most essential features of GitLab:
- Dev
- Approvals
@ -62,8 +63,7 @@ DevOps Adoption shows you which groups within your organization are using the mo
- Pipelines
- Runners
When managing groups in the UI, you can add your groups with the **Add group to table**
button, in the top right hand section the page.
To add your groups, in the top right-hand section the page, select **Add group to table**.
DevOps Adoption allows you to:

View file

@ -25,10 +25,6 @@ The Compliance Dashboard shows only the latest MR on each project.
## Merge request drawer
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299357) in GitLab 14.1.
> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-merge-request-drawer).
When you click on a row, a drawer is shown that provides further details about the merge
request:
@ -104,28 +100,3 @@ the dropdown next to the **List of all merge commits** button at the top of the
NOTE:
The Chain of Custody report download is a CSV file, with a maximum size of 15 MB.
The remaining records are truncated when this limit is reached.
## Enable or disable merge request drawer **(ULTIMATE SELF)**
The merge request drawer is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
# For the instance
Feature.enable(:compliance_dashboard_drawer)
# For a single group
Feature.enable(:compliance_dashboard_drawer, Group.find(<group id>))
```
To disable it:
```ruby
# For the instance
Feature.disable(:compliance_dashboard_drawer)
# For a single group
Feature.disable(:compliance_dashboard_drawer, Group.find(<group id>)
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -110,7 +110,7 @@ on an existing group's page.
1. On the New Group page, select **Import group**.
![Fill in import details](img/import_panel_v13_8.png)
![Fill in import details](img/import_panel_v14_1.png)
1. Fill in source URL of your GitLab.
1. Fill in [personal access token](../../../user/profile/personal_access_tokens.md) for remote GitLab instance.
@ -129,4 +129,4 @@ Migration importer page. Listed are the remote GitLab groups to which you have t
1. Once a group has been imported, click its GitLab path to open its GitLab URL.
![Group Importer page](img/bulk_imports_v13_8.png)
![Group Importer page](img/bulk_imports_v14_1.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -93,9 +93,9 @@ on an existing group's page.
![Navigation paths to create a new group](img/new_group_navigation_v13_1.png)
1. On the New Group page, select the **Import group** tab.
1. On the New Group page, select the **Import group**.
![Fill in group details](img/import_panel_v13_4.png)
![Fill in group details](img/import_panel_v14_1.png)
1. Enter your group name.

View file

@ -104,6 +104,7 @@ Without the approvals, the work cannot merge. Required approvals enable multiple
database, for all proposed code changes.
- Use the [code owners of changed files](rules.md#code-owners-as-eligible-approvers),
to determine who should review the work.
- Require an [approval before merging code that causes test coverage to decline](../../../../ci/pipelines/settings.md#coverage-check-approval-rule)
- [Require approval from a security team](../../../application_security/index.md#security-approvals-in-merge-requests)
before merging code that could introduce a vulnerability. **(ULTIMATE)**

View file

@ -152,6 +152,20 @@ renames a Git repository's (`example`) default branch.
1. Update references to the old branch name in related code and scripts that reside outside
your repository, such as helper utilities and integrations.
## Default branch rename redirect
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/329100) in GitLab 14.1
URLs for specific files or directories in a project embed the project's default
branch name, and are often found in documentation or browser bookmarks. When you
[update the default branch name in your repository](#update-the-default-branch-name-in-your-repository),
these URLs change, and must be updated.
To ease the transition period, whenever the default branch for a project is
changed, GitLab records the name of the old default branch. If that branch is
deleted, attempts to view a file or directory on it are redirected to the
current default branch, instead of displaying the "not found" page.
## Resources
- [Discussion of default branch renaming](https://lore.kernel.org/git/pull.656.v4.git.1593009996.gitgitgadget@gmail.com/)

View file

@ -26,17 +26,17 @@ module ExtractsPath
# Automatically renders `not_found!` if a valid tree path could not be
# resolved (e.g., when a user inserts an invalid path or ref).
#
# Automatically redirects to the current default branch if the ref matches a
# previous default branch that has subsequently been deleted.
#
# rubocop:disable Gitlab/ModuleWithInstanceVariables
override :assign_ref_vars
def assign_ref_vars
super
if @path.empty? && !@commit && @id.ends_with?('.atom')
@id = @ref = extract_ref_without_atom(@id)
@commit = @repo.commit(@ref)
rectify_atom!
request.format = :atom if @commit
end
rectify_renamed_default_branch! && return
raise InvalidPathError unless @commit
@ -59,6 +59,42 @@ module ExtractsPath
private
# Override in controllers to determine which actions are subject to the redirect
def redirect_renamed_default_branch?
false
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def rectify_atom!
return if @commit
return unless @id.ends_with?('.atom')
return unless @path.empty?
@id = @ref = extract_ref_without_atom(@id)
@commit = @repo.commit(@ref)
request.format = :atom if @commit
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
# For GET/HEAD requests, if the ref doesn't exist in the repository, check
# whether we're trying to access a renamed default branch. If we are, we can
# redirect to the current default branch instead of rendering a 404.
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def rectify_renamed_default_branch!
return unless redirect_renamed_default_branch?
return if @commit
return unless @id && @ref && repository_container.respond_to?(:previous_default_branch)
return unless repository_container.previous_default_branch == @ref
return unless request.get? || request.head?
flash[:notice] = _('The default branch for this project has been changed. Please update your bookmarks.')
redirect_to url_for(id: @id.sub(/\A#{Regexp.escape(@ref)}/, repository_container.default_branch))
true
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
override :repository_container
def repository_container
@project

View file

@ -15,6 +15,10 @@ module Gitlab
:updated_at_asc
when %w[updated_at desc], [nil, 'updated_desc']
:updated_at_desc
when %w[popularity asc], [nil, 'popularity_asc']
:popularity_asc
when %w[popularity desc], [nil, 'popularity_desc']
:popularity_desc
else
:unknown
end

View file

@ -7,6 +7,11 @@ module Gitlab
DEFAULT_PAGE = 1
DEFAULT_PER_PAGE = 20
SCOPE_ONLY_SORT = {
popularity_asc: %w[issues],
popularity_desc: %w[issues]
}.freeze
attr_reader :current_user, :query, :order_by, :sort, :filters
# Limit search results by passed projects
@ -128,20 +133,29 @@ module Gitlab
end
# rubocop: disable CodeReuse/ActiveRecord
def apply_sort(scope)
def apply_sort(results, scope: nil)
# Due to different uses of sort param we prefer order_by when
# present
case ::Gitlab::Search::SortOptions.sort_and_direction(order_by, sort)
sort_by = ::Gitlab::Search::SortOptions.sort_and_direction(order_by, sort)
# Reset sort to default if the chosen one is not supported by scope
sort_by = nil if SCOPE_ONLY_SORT[sort_by] && !SCOPE_ONLY_SORT[sort_by].include?(scope)
case sort_by
when :created_at_asc
scope.reorder('created_at ASC')
results.reorder('created_at ASC')
when :created_at_desc
scope.reorder('created_at DESC')
results.reorder('created_at DESC')
when :updated_at_asc
scope.reorder('updated_at ASC')
results.reorder('updated_at ASC')
when :updated_at_desc
scope.reorder('updated_at DESC')
results.reorder('updated_at DESC')
when :popularity_asc
results.reorder('upvotes_count ASC')
when :popularity_desc
results.reorder('upvotes_count DESC')
else
scope.reorder('created_at DESC')
results.reorder('created_at DESC')
end
end
# rubocop: enable CodeReuse/ActiveRecord
@ -157,7 +171,7 @@ module Gitlab
issues = issues.where(project_id: project_ids_relation) # rubocop: disable CodeReuse/ActiveRecord
end
apply_sort(issues)
apply_sort(issues, scope: 'issues')
end
# rubocop: disable CodeReuse/ActiveRecord
@ -177,7 +191,7 @@ module Gitlab
merge_requests = merge_requests.in_projects(project_ids_relation)
end
apply_sort(merge_requests)
apply_sort(merge_requests, scope: 'merge_requests')
end
def default_scope

View file

@ -1459,12 +1459,6 @@ msgstr ""
msgid "A member of the abuse team will review your report as soon as possible."
msgstr ""
msgid "A merge request approval is required when a security report contains a new vulnerability of high, critical, or unknown severity."
msgstr ""
msgid "A merge request approval is required when the license compliance report contains a denied license."
msgstr ""
msgid "A merge request hasn't yet been merged"
msgstr ""
@ -5681,6 +5675,9 @@ msgstr ""
msgid "BulkImport|Existing groups"
msgstr ""
msgid "BulkImport|Filter by source group"
msgstr ""
msgid "BulkImport|From source group"
msgstr ""
@ -19290,15 +19287,9 @@ msgstr ""
msgid "Learn more about Auto DevOps"
msgstr ""
msgid "Learn more about License-Check"
msgstr ""
msgid "Learn more about Needs relationships"
msgstr ""
msgid "Learn more about Vulnerability-Check"
msgstr ""
msgid "Learn more about Web Terminal"
msgstr ""
@ -19494,9 +19485,6 @@ msgstr ""
msgid "License overview"
msgstr ""
msgid "License-Check"
msgstr ""
msgid "LicenseCompliance|%{docLinkStart}License Approvals%{docLinkEnd} are active"
msgstr ""
@ -28931,18 +28919,51 @@ msgstr ""
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability of high, critical, or unknown severity."
msgstr ""
msgid "SecurityApprovals|A merge request approval is required when test coverage declines."
msgstr ""
msgid "SecurityApprovals|A merge request approval is required when the license compliance report contains a denied license."
msgstr ""
msgid "SecurityApprovals|Configurable if security scanners are enabled. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
msgid "SecurityApprovals|Coverage-Check"
msgstr ""
msgid "SecurityApprovals|Learn more about Coverage-Check"
msgstr ""
msgid "SecurityApprovals|Learn more about License-Check"
msgstr ""
msgid "SecurityApprovals|Learn more about Vulnerability-Check"
msgstr ""
msgid "SecurityApprovals|License Scanning must be enabled. %{linkStart}Learn more%{linkEnd}."
msgstr ""
msgid "SecurityApprovals|License-Check"
msgstr ""
msgid "SecurityApprovals|Requires approval for Denied licenses. %{linkStart}More information%{linkEnd}"
msgstr ""
msgid "SecurityApprovals|Requires approval for decreases in test coverage. %{linkStart}More information%{linkEnd}"
msgstr ""
msgid "SecurityApprovals|Requires approval for vulnerabilities of Critical, High, or Unknown severity. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
msgid "SecurityApprovals|Test coverage must be enabled. %{linkStart}Learn more%{linkEnd}."
msgstr ""
msgid "SecurityApprovals|Vulnerability-Check"
msgstr ""
msgid "SecurityConfiguration|%{featureName} merge request creation mutation failed"
msgstr ""
@ -29765,7 +29786,7 @@ msgstr ""
msgid "Service URL"
msgstr ""
msgid "Service ping is disabled, and cannot be configured through this form."
msgid "Service ping is disabled in your configuration file, and cannot be enabled through this form."
msgstr ""
msgid "ServiceDesk|Enable Service Desk"
@ -32554,6 +32575,9 @@ msgstr ""
msgid "The default CI/CD configuration file and path for new projects."
msgstr ""
msgid "The default branch for this project has been changed. Please update your bookmarks."
msgstr ""
msgid "The dependency list details information about the components used within your project."
msgstr ""
@ -36400,9 +36424,6 @@ msgstr ""
msgid "Vulnerability resolved in the default branch"
msgstr ""
msgid "Vulnerability-Check"
msgstr ""
msgid "VulnerabilityChart|%{formattedStartDate} to today"
msgstr ""
@ -37923,7 +37944,7 @@ msgstr ""
msgid "Your CSV import for project"
msgstr ""
msgid "Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. View how you compare with other organizations, discover features you are not using, and learn best practices through blog posts and white papers."
msgid "Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. Use it to view how you compare with other organizations."
msgstr ""
msgid "Your GPG keys (%{count})"

View file

@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe Projects::BlobController do
include ProjectForksHelper
let(:project) { create(:project, :public, :repository) }
let(:project) { create(:project, :public, :repository, previous_default_branch: previous_default_branch) }
let(:previous_default_branch) { nil }
describe "GET show" do
def request
@ -42,6 +43,20 @@ RSpec.describe Projects::BlobController do
it { is_expected.to respond_with(:not_found) }
end
context "renamed default branch, valid file" do
let(:id) { 'old-default-branch/README.md' }
let(:previous_default_branch) { 'old-default-branch' }
it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/README.md") }
end
context "renamed default branch, invalid file" do
let(:id) { 'old-default-branch/invalid-path.rb' }
let(:previous_default_branch) { 'old-default-branch' }
it { is_expected.to redirect_to("/#{project.full_path}/-/blob/#{project.default_branch}/invalid-path.rb") }
end
context "binary file" do
let(:id) { 'binary-encoding/encoding/binary-1.bin' }

View file

@ -3,8 +3,9 @@
require 'spec_helper'
RSpec.describe Projects::TreeController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:project) { create(:project, :repository, previous_default_branch: previous_default_branch) }
let(:previous_default_branch) { nil }
let(:user) { create(:user) }
before do
sign_in(user)
@ -55,6 +56,20 @@ RSpec.describe Projects::TreeController do
it { is_expected.to respond_with(:not_found) }
end
context "renamed default branch, valid file" do
let(:id) { 'old-default-branch/encoding/' }
let(:previous_default_branch) { 'old-default-branch' }
it { is_expected.to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/encoding/") }
end
context "renamed default branch, invalid file" do
let(:id) { 'old-default-branch/invalid-path/' }
let(:previous_default_branch) { 'old-default-branch' }
it { is_expected.to redirect_to("/#{project.full_path}/-/tree/#{project.default_branch}/invalid-path/") }
end
context "valid empty branch, invalid path" do
let(:id) { 'empty-branch/invalid-path/' }

View file

@ -3,6 +3,19 @@
FactoryBot.define do
factory :merge_request_cleanup_schedule, class: 'MergeRequest::CleanupSchedule' do
merge_request
scheduled_at { Time.current }
scheduled_at { 1.day.ago }
trait :running do
status { MergeRequest::CleanupSchedule::STATUSES[:running] }
end
trait :completed do
status { MergeRequest::CleanupSchedule::STATUSES[:completed] }
completed_at { Time.current }
end
trait :failed do
status { MergeRequest::CleanupSchedule::STATUSES[:failed] }
end
end
end

View file

@ -12,7 +12,7 @@ milestone: "13.9"
introduced_by_url:
time_frame: 7d
data_source:
data_category: Operational
data_category: Optional
distribution:
- ee
tier:

View file

@ -13,7 +13,7 @@ milestone: "13.9"
introduced_by_url:
time_frame: 7d
data_source:
data_category: Operational
data_category: Optional
distribution:
- ce
- ee

View file

@ -1,8 +1,8 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
const DEFAULT_PROPS = {
@ -10,6 +10,8 @@ const DEFAULT_PROPS = {
path: 'some/path',
canPushCode: true,
replacePath: 'some/replace/path',
deletePath: 'some/delete/path',
emptyRepo: false,
};
const DEFAULT_INJECT = {
@ -39,6 +41,7 @@ describe('BlobButtonGroup component', () => {
wrapper.destroy();
});
const findDeleteBlobModal = () => wrapper.findComponent(DeleteBlobModal);
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
const findReplaceButton = () => wrapper.findAll(GlButton).at(0);
@ -93,4 +96,22 @@ describe('BlobButtonGroup component', () => {
primaryBtnText: 'Replace file',
});
});
it('renders DeleteBlobModel', () => {
createComponent();
const { targetBranch, originalBranch } = DEFAULT_INJECT;
const { name, canPushCode, deletePath, emptyRepo } = DEFAULT_PROPS;
const title = `Delete ${name}`;
expect(findDeleteBlobModal().props()).toMatchObject({
modalTitle: title,
commitMessage: title,
targetBranch,
originalBranch,
canPushCode,
deletePath,
emptyRepo,
});
});
});

View file

@ -58,23 +58,36 @@ const richMockData = {
renderError: null,
},
};
const userPermissionsMockData = {
const projectMockData = {
userPermissions: {
pushCode: true,
},
repository: {
empty: false,
},
};
const localVue = createLocalVue();
const mockAxios = new MockAdapter(axios);
const createComponentWithApollo = (mockData, mockPermissionData = true) => {
const createComponentWithApollo = (mockData = {}) => {
localVue.use(VueApollo);
const defaultPushCode = projectMockData.userPermissions.pushCode;
const defaultEmptyRepo = projectMockData.repository.empty;
const { blobs, emptyRepo = defaultEmptyRepo, canPushCode = defaultPushCode } = mockData;
const mockResolver = jest.fn().mockResolvedValue({
data: {
project: {
userPermissions: { pushCode: mockPermissionData },
repository: { blobs: { nodes: [mockData] } },
userPermissions: { pushCode: canPushCode },
repository: {
empty: emptyRepo,
blobs: {
nodes: [blobs],
},
},
},
},
});
@ -209,14 +222,14 @@ describe('Blob content viewer component', () => {
describe('legacy viewers', () => {
it('does not load a legacy viewer when a rich viewer is not available', async () => {
createComponentWithApollo(simpleMockData);
createComponentWithApollo({ blobs: simpleMockData });
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(0);
});
it('loads a legacy viewer when a rich viewer is available', async () => {
createComponentWithApollo(richMockData);
createComponentWithApollo({ blobs: richMockData });
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(1);
@ -320,16 +333,20 @@ describe('Blob content viewer component', () => {
});
describe('BlobButtonGroup', () => {
const { name, path, replacePath } = simpleMockData;
const { name, path, replacePath, webPath } = simpleMockData;
const {
userPermissions: { pushCode },
} = userPermissionsMockData;
repository: { empty },
} = projectMockData;
it('renders component', async () => {
window.gon.current_user_id = 1;
fullFactory({
mockData: { blobInfo: simpleMockData, project: userPermissionsMockData },
mockData: {
blobInfo: simpleMockData,
project: { userPermissions: { pushCode }, repository: { empty } },
},
stubs: {
BlobContent: true,
BlobButtonGroup: true,
@ -342,7 +359,9 @@ describe('Blob content viewer component', () => {
name,
path,
replacePath,
deletePath: webPath,
canPushCode: pushCode,
emptyRepo: empty,
});
});

View file

@ -0,0 +1,130 @@
import { GlFormTextarea, GlModal, GlFormInput, GlToggle } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
const initialProps = {
modalId: 'Delete-blob',
modalTitle: 'Delete File',
deletePath: 'some/path',
commitMessage: 'Delete File',
targetBranch: 'some-target-branch',
originalBranch: 'main',
canPushCode: true,
emptyRepo: false,
};
describe('DeleteBlobModal', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(DeleteBlobModal, {
propsData: {
...initialProps,
...props,
},
});
};
const findModal = () => wrapper.findComponent(GlModal);
const findForm = () => wrapper.findComponent({ ref: 'form' });
afterEach(() => {
wrapper.destroy();
});
it('renders Modal component', () => {
createComponent();
const { modalTitle: title } = initialProps;
expect(findModal().props()).toMatchObject({
title,
size: 'md',
actionPrimary: {
text: 'Delete file',
},
actionCancel: {
text: 'Cancel',
},
});
});
describe('form', () => {
it('gets passed the path for action attribute', () => {
createComponent();
expect(findForm().attributes('action')).toBe(initialProps.deletePath);
});
it('submits the form', async () => {
createComponent();
const submitSpy = jest.spyOn(findForm().element, 'submit');
findModal().vm.$emit('primary', { preventDefault: () => {} });
await nextTick();
expect(submitSpy).toHaveBeenCalled();
submitSpy.mockRestore();
});
it.each`
component | defaultValue | canPushCode | targetBranch | originalBranch | exist
${GlFormTextarea} | ${initialProps.commitMessage} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
${GlFormInput} | ${initialProps.targetBranch} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
${GlFormInput} | ${undefined} | ${false} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${false}
${GlToggle} | ${'true'} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true}
${GlToggle} | ${undefined} | ${true} | ${'same-branch'} | ${'same-branch'} | ${false}
`(
'has the correct form fields ',
({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => {
createComponent({
canPushCode,
targetBranch,
originalBranch,
});
const formField = wrapper.findComponent(component);
if (!exist) {
expect(formField.exists()).toBe(false);
return;
}
expect(formField.exists()).toBe(true);
expect(formField.attributes('value')).toBe(defaultValue);
},
);
it.each`
input | value | emptyRepo | canPushCode | exist
${'authenticity_token'} | ${'mock-csrf-token'} | ${false} | ${true} | ${true}
${'authenticity_token'} | ${'mock-csrf-token'} | ${true} | ${false} | ${true}
${'_method'} | ${'delete'} | ${false} | ${true} | ${true}
${'_method'} | ${'delete'} | ${true} | ${false} | ${true}
${'original_branch'} | ${initialProps.originalBranch} | ${false} | ${true} | ${true}
${'original_branch'} | ${undefined} | ${true} | ${true} | ${false}
${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${true}
${'create_merge_request'} | ${'1'} | ${false} | ${true} | ${true}
${'create_merge_request'} | ${undefined} | ${true} | ${false} | ${false}
`(
'passes $input as a hidden input with the correct value',
({ input, value, emptyRepo, canPushCode, exist }) => {
createComponent({
emptyRepo,
canPushCode,
});
const inputMethod = findForm().find(`input[name="${input}"]`);
if (!exist) {
expect(inputMethod.exists()).toBe(false);
return;
}
expect(inputMethod.attributes('type')).toBe('hidden');
expect(inputMethod.attributes('value')).toBe(value);
},
);
});
});

View file

@ -3,7 +3,7 @@ import { mapComputed } from '~/vuex_shared/bindings';
describe('Binding utils', () => {
describe('mapComputed', () => {
const defaultArgs = [['baz'], 'bar', 'foo'];
const defaultArgs = [['baz'], 'bar', 'foo', 'qux'];
const createDummy = (mapComputedArgs = defaultArgs) => ({
computed: {
@ -29,12 +29,18 @@ describe('Binding utils', () => {
},
};
it('returns an object with keys equal to the first fn parameter ', () => {
it('returns an object with keys equal to the first fn parameter', () => {
const keyList = ['foo1', 'foo2'];
const result = mapComputed(keyList, 'foo', 'bar');
expect(Object.keys(result)).toEqual(keyList);
});
it('returns an object with keys equal to the first fn parameter when the root is a function', () => {
const keyList = ['foo1', 'foo2'];
const result = mapComputed(keyList, 'foo', (state) => state.bar);
expect(Object.keys(result)).toEqual(keyList);
});
it('returned object has set and get function', () => {
const result = mapComputed(['baz'], 'foo', 'bar');
expect(result.baz.set).toBeDefined();

View file

@ -75,6 +75,13 @@ RSpec.describe ClustersHelper do
it 'displays project path' do
expect(subject[:project_path]).to eq(project.full_path)
end
it 'generates docs urls' do
expect(subject[:agent_docs_url]).to eq(help_page_path('user/clusters/agent/index'))
expect(subject[:install_docs_url]).to eq(help_page_path('administration/clusters/kas'))
expect(subject[:get_started_docs_url]).to eq(help_page_path('user/clusters/agent/index', anchor: 'define-a-configuration-repository'))
expect(subject[:integration_docs_url]).to eq(help_page_path('user/clusters/agent/index', anchor: 'get-started-with-gitops-and-the-gitlab-agent'))
end
end
describe '#js_clusters_list_data' do

View file

@ -7,10 +7,17 @@ RSpec.describe ExtractsPath do
include RepoHelpers
include Gitlab::Routing
# Make url_for work
def default_url_options
{ controller: 'projects/blob', action: 'show', namespace_id: @project.namespace.path, project_id: @project.path }
end
let_it_be(:owner) { create(:user) }
let_it_be(:container) { create(:project, :repository, creator: owner) }
let(:request) { double('request') }
let(:flash) { {} }
let(:redirect_renamed_default_branch?) { true }
before do
@project = container
@ -18,11 +25,14 @@ RSpec.describe ExtractsPath do
allow(container.repository).to receive(:ref_names).and_return(ref_names)
allow(request).to receive(:format=)
allow(request).to receive(:get?)
allow(request).to receive(:head?)
end
describe '#assign_ref_vars' do
let(:ref) { sample_commit[:id] }
let(:params) { { path: sample_commit[:line_code_path], ref: ref } }
let(:path) { sample_commit[:line_code_path] }
let(:params) { { path: path, ref: ref } }
it_behaves_like 'assigns ref vars'
@ -126,6 +136,66 @@ RSpec.describe ExtractsPath do
expect(@commit).to be_nil
end
end
context 'ref points to a previous default branch' do
let(:ref) { 'develop' }
before do
@project.update!(previous_default_branch: ref)
allow(@project).to receive(:default_branch).and_return('foo')
end
it 'redirects to the new default branch for a GET request' do
allow(request).to receive(:get?).and_return(true)
expect(self).to receive(:redirect_to).with("http://localhost/#{@project.full_path}/-/blob/foo/#{path}")
expect(self).not_to receive(:render_404)
assign_ref_vars
expect(@commit).to be_nil
expect(flash[:notice]).to match(/default branch/)
end
it 'redirects to the new default branch for a HEAD request' do
allow(request).to receive(:head?).and_return(true)
expect(self).to receive(:redirect_to).with("http://localhost/#{@project.full_path}/-/blob/foo/#{path}")
expect(self).not_to receive(:render_404)
assign_ref_vars
expect(@commit).to be_nil
expect(flash[:notice]).to match(/default branch/)
end
it 'returns 404 for any other request type' do
expect(self).not_to receive(:redirect_to)
expect(self).to receive(:render_404)
assign_ref_vars
expect(@commit).to be_nil
expect(flash).to be_empty
end
context 'redirect behaviour is disabled' do
let(:redirect_renamed_default_branch?) { false }
it 'returns 404 for a GET request' do
allow(request).to receive(:get?).and_return(true)
expect(self).not_to receive(:redirect_to)
expect(self).to receive(:render_404)
assign_ref_vars
expect(@commit).to be_nil
expect(flash).to be_empty
end
end
end
end
it_behaves_like 'extracts refs'

View file

@ -229,10 +229,18 @@ RSpec.describe Gitlab::SearchResults do
let!(:new_updated) { create(:issue, project: project, title: 'updated recent', updated_at: 1.day.ago) }
let!(:very_old_updated) { create(:issue, project: project, title: 'updated very old', updated_at: 1.year.ago) }
let!(:less_popular_result) { create(:issue, project: project, title: 'less popular', upvotes_count: 10) }
let!(:popular_result) { create(:issue, project: project, title: 'popular', upvotes_count: 100) }
let!(:non_popular_result) { create(:issue, project: project, title: 'non popular', upvotes_count: 1) }
include_examples 'search results sorted' do
let(:results_created) { described_class.new(user, 'sorted', Project.order(:id), sort: sort, filters: filters) }
let(:results_updated) { described_class.new(user, 'updated', Project.order(:id), sort: sort, filters: filters) }
end
include_examples 'search results sorted by popularity' do
let(:results_popular) { described_class.new(user, 'popular', Project.order(:id), sort: sort, filters: filters) }
end
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe AddUpvotesCountIndexToIssues do
let(:migration_instance) { described_class.new }
describe '#up' do
it 'adds index' do
expect { migrate! }.to change { migration_instance.index_exists?(:issues, [:project_id, :upvotes_count], name: described_class::INDEX_NAME) }.from(false).to(true)
end
end
describe '#down' do
it 'removes index' do
migrate!
expect { schema_migrate_down! }.to change { migration_instance.index_exists?(:issues, [:project_id, :upvotes_count], name: described_class::INDEX_NAME) }.from(true).to(false)
end
end
end

View file

@ -11,22 +11,125 @@ RSpec.describe MergeRequest::CleanupSchedule do
it { is_expected.to validate_presence_of(:scheduled_at) }
end
describe '.scheduled_merge_request_ids' do
let_it_be(:mr_cleanup_schedule_1) { create(:merge_request_cleanup_schedule, scheduled_at: 2.days.ago) }
let_it_be(:mr_cleanup_schedule_2) { create(:merge_request_cleanup_schedule, scheduled_at: 1.day.ago) }
let_it_be(:mr_cleanup_schedule_3) { create(:merge_request_cleanup_schedule, scheduled_at: 1.day.ago, completed_at: Time.current) }
let_it_be(:mr_cleanup_schedule_4) { create(:merge_request_cleanup_schedule, scheduled_at: 4.days.ago) }
let_it_be(:mr_cleanup_schedule_5) { create(:merge_request_cleanup_schedule, scheduled_at: 3.days.ago) }
let_it_be(:mr_cleanup_schedule_6) { create(:merge_request_cleanup_schedule, scheduled_at: 1.day.from_now) }
let_it_be(:mr_cleanup_schedule_7) { create(:merge_request_cleanup_schedule, scheduled_at: 5.days.ago) }
describe 'state machine transitions' do
let(:cleanup_schedule) { create(:merge_request_cleanup_schedule) }
it 'only includes incomplete schedule within the specified limit' do
expect(described_class.scheduled_merge_request_ids(4)).to eq([
mr_cleanup_schedule_2.merge_request_id,
mr_cleanup_schedule_1.merge_request_id,
mr_cleanup_schedule_5.merge_request_id,
mr_cleanup_schedule_4.merge_request_id
it 'sets status to unstarted by default' do
expect(cleanup_schedule).to be_unstarted
end
describe '#run' do
it 'sets the status to running' do
cleanup_schedule.run
expect(cleanup_schedule.reload).to be_running
end
context 'when previous status is not unstarted' do
let(:cleanup_schedule) { create(:merge_request_cleanup_schedule, :running) }
it 'does not change status' do
expect { cleanup_schedule.run }.not_to change(cleanup_schedule, :status)
end
end
end
describe '#retry' do
let(:cleanup_schedule) { create(:merge_request_cleanup_schedule, :running) }
it 'sets the status to unstarted' do
cleanup_schedule.retry
expect(cleanup_schedule.reload).to be_unstarted
end
it 'increments failed_count' do
expect { cleanup_schedule.retry }.to change(cleanup_schedule, :failed_count).by(1)
end
context 'when previous status is not running' do
let(:cleanup_schedule) { create(:merge_request_cleanup_schedule) }
it 'does not change status' do
expect { cleanup_schedule.retry }.not_to change(cleanup_schedule, :status)
end
end
end
describe '#complete' do
let(:cleanup_schedule) { create(:merge_request_cleanup_schedule, :running) }
it 'sets the status to completed' do
cleanup_schedule.complete
expect(cleanup_schedule.reload).to be_completed
end
it 'sets the completed_at' do
expect { cleanup_schedule.complete }.to change(cleanup_schedule, :completed_at)
end
context 'when previous status is not running' do
let(:cleanup_schedule) { create(:merge_request_cleanup_schedule, :completed) }
it 'does not change status' do
expect { cleanup_schedule.complete }.not_to change(cleanup_schedule, :status)
end
end
end
describe '#mark_as_failed' do
let(:cleanup_schedule) { create(:merge_request_cleanup_schedule, :running) }
it 'sets the status to failed' do
cleanup_schedule.mark_as_failed
expect(cleanup_schedule.reload).to be_failed
end
it 'increments failed_count' do
expect { cleanup_schedule.mark_as_failed }.to change(cleanup_schedule, :failed_count).by(1)
end
context 'when previous status is not running' do
let(:cleanup_schedule) { create(:merge_request_cleanup_schedule, :failed) }
it 'does not change status' do
expect { cleanup_schedule.mark_as_failed }.not_to change(cleanup_schedule, :status)
end
end
end
end
describe '.scheduled_and_unstarted' do
let!(:cleanup_schedule_1) { create(:merge_request_cleanup_schedule, scheduled_at: 2.days.ago) }
let!(:cleanup_schedule_2) { create(:merge_request_cleanup_schedule, scheduled_at: 1.day.ago) }
let!(:cleanup_schedule_3) { create(:merge_request_cleanup_schedule, :completed, scheduled_at: 1.day.ago) }
let!(:cleanup_schedule_4) { create(:merge_request_cleanup_schedule, scheduled_at: 4.days.ago) }
let!(:cleanup_schedule_5) { create(:merge_request_cleanup_schedule, scheduled_at: 3.days.ago) }
let!(:cleanup_schedule_6) { create(:merge_request_cleanup_schedule, scheduled_at: 1.day.from_now) }
let!(:cleanup_schedule_7) { create(:merge_request_cleanup_schedule, :failed, scheduled_at: 5.days.ago) }
it 'returns records that are scheduled before or on current time and unstarted (ordered by scheduled first)' do
expect(described_class.scheduled_and_unstarted).to eq([
cleanup_schedule_2,
cleanup_schedule_1,
cleanup_schedule_5,
cleanup_schedule_4
])
end
end
describe '.start_next' do
let!(:cleanup_schedule_1) { create(:merge_request_cleanup_schedule, :completed, scheduled_at: 1.day.ago) }
let!(:cleanup_schedule_2) { create(:merge_request_cleanup_schedule, scheduled_at: 2.days.ago) }
let!(:cleanup_schedule_3) { create(:merge_request_cleanup_schedule, :running, scheduled_at: 1.day.ago) }
let!(:cleanup_schedule_4) { create(:merge_request_cleanup_schedule, scheduled_at: 3.days.ago) }
let!(:cleanup_schedule_5) { create(:merge_request_cleanup_schedule, :failed, scheduled_at: 3.days.ago) }
it 'finds the next scheduled and unstarted then marked it as running' do
expect(described_class.start_next).to eq(cleanup_schedule_2)
expect(cleanup_schedule_2.reload).to be_running
end
end
end

View file

@ -137,6 +137,7 @@ project_setting:
- has_confluence
- has_vulnerabilities
- prevent_merge_without_jira_issue
- previous_default_branch
- project_id
- push_rule_id
- show_default_award_emojis

View file

@ -200,17 +200,32 @@ RSpec.describe Projects::UpdateService do
context 'when updating a default branch' do
let(:project) { create(:project, :repository) }
it 'changes a default branch' do
it 'changes default branch, tracking the previous branch' do
previous_default_branch = project.default_branch
update_project(project, admin, default_branch: 'feature')
expect(Project.find(project.id).default_branch).to eq 'feature'
project.reload
expect(project.default_branch).to eq('feature')
expect(project.previous_default_branch).to eq(previous_default_branch)
update_project(project, admin, default_branch: previous_default_branch)
project.reload
expect(project.default_branch).to eq(previous_default_branch)
expect(project.previous_default_branch).to eq('feature')
end
it 'does not change a default branch' do
# The branch 'unexisted-branch' does not exist.
update_project(project, admin, default_branch: 'unexisted-branch')
expect(Project.find(project.id).default_branch).to eq 'master'
project.reload
expect(project.default_branch).to eq 'master'
expect(project.previous_default_branch).to be_nil
end
end

View file

@ -33,3 +33,21 @@ RSpec.shared_examples 'search results sorted' do
end
end
end
RSpec.shared_examples 'search results sorted by popularity' do
context 'sort: popularity_desc' do
let(:sort) { 'popularity_desc' }
it 'sorts results by upvotes' do
expect(results_popular.objects(scope).map(&:id)).to eq([popular_result.id, less_popular_result.id, non_popular_result.id])
end
end
context 'sort: popularity_asc' do
let(:sort) { 'popularity_asc' }
it 'sorts results by created_at' do
expect(results_popular.objects(scope).map(&:id)).to eq([non_popular_result.id, less_popular_result.id, popular_result.id])
end
end
end

View file

@ -64,6 +64,7 @@ RSpec.describe 'projects/empty' do
expect(rendered).to have_selector('.js-invite-members-modal')
expect(rendered).to have_selector('[data-label=invite_members_empty_project]')
expect(rendered).to have_selector('[data-event=click_button]')
expect(rendered).to have_selector('[data-trigger-source=project-empty-page]')
end
context 'when user does not have permissions to invite members' do

View file

@ -418,6 +418,7 @@ RSpec.describe 'Every Sidekiq worker' do
'ScanSecurityReportSecretsWorker' => 17,
'Security::AutoFixWorker' => 3,
'Security::StoreScansWorker' => 3,
'Security::TrackSecureScansWorker' => 1,
'SelfMonitoringProjectCreateWorker' => 3,
'SelfMonitoringProjectDeleteWorker' => 3,
'ServiceDeskEmailReceiverWorker' => 3,

View file

@ -3,18 +3,41 @@
require 'spec_helper'
RSpec.describe MergeRequestCleanupRefsWorker do
describe '#perform' do
context 'when merge request exists' do
let(:merge_request) { create(:merge_request) }
let(:job_args) { merge_request.id }
let(:worker) { described_class.new }
include_examples 'an idempotent worker' do
it 'calls MergeRequests::CleanupRefsService#execute' do
expect_next_instance_of(MergeRequests::CleanupRefsService, merge_request) do |svc|
expect(svc).to receive(:execute).and_call_original
end.twice
describe '#perform_work' do
context 'when next cleanup schedule is found' do
let(:failed_count) { 0 }
let!(:cleanup_schedule) { create(:merge_request_cleanup_schedule, failed_count: failed_count) }
subject
it 'marks the cleanup schedule as completed on success' do
stub_cleanup_service(status: :success)
worker.perform_work
expect(cleanup_schedule.reload).to be_completed
expect(cleanup_schedule.completed_at).to be_present
end
context 'when service fails' do
before do
stub_cleanup_service(status: :error)
worker.perform_work
end
it 'marks the cleanup schedule as unstarted and track the failure' do
expect(cleanup_schedule.reload).to be_unstarted
expect(cleanup_schedule.failed_count).to eq(1)
expect(cleanup_schedule.completed_at).to be_nil
end
context "and cleanup schedule has already failed #{described_class::FAILURE_THRESHOLD} times" do
let(:failed_count) { described_class::FAILURE_THRESHOLD }
it 'marks the cleanup schedule as failed and track the failure' do
expect(cleanup_schedule.reload).to be_failed
expect(cleanup_schedule.failed_count).to eq(described_class::FAILURE_THRESHOLD + 1)
expect(cleanup_schedule.completed_at).to be_nil
end
end
end
@ -23,20 +46,52 @@ RSpec.describe MergeRequestCleanupRefsWorker do
stub_feature_flags(merge_request_refs_cleanup: false)
end
it 'does not clean up the merge request' do
it 'does nothing' do
expect(MergeRequests::CleanupRefsService).not_to receive(:new)
perform_multiple(1)
worker.perform_work
end
end
end
context 'when merge request does not exist' do
it 'does not call MergeRequests::CleanupRefsService' do
context 'when there is no next cleanup schedule found' do
it 'does nothing' do
expect(MergeRequests::CleanupRefsService).not_to receive(:new)
perform_multiple(1)
worker.perform_work
end
end
end
describe '#remaining_work_count' do
let_it_be(:unstarted) { create_list(:merge_request_cleanup_schedule, 2) }
let_it_be(:running) { create_list(:merge_request_cleanup_schedule, 2, :running) }
let_it_be(:completed) { create_list(:merge_request_cleanup_schedule, 2, :completed) }
it 'returns number of scheduled and unstarted cleanup schedule records' do
expect(worker.remaining_work_count).to eq(unstarted.count)
end
context 'when count exceeds max_running_jobs' do
before do
create_list(:merge_request_cleanup_schedule, worker.max_running_jobs)
end
it 'gets capped at max_running_jobs' do
expect(worker.remaining_work_count).to eq(worker.max_running_jobs)
end
end
end
describe '#max_running_jobs' do
it 'returns the value of MAX_RUNNING_JOBS' do
expect(worker.max_running_jobs).to eq(described_class::MAX_RUNNING_JOBS)
end
end
def stub_cleanup_service(result)
expect_next_instance_of(MergeRequests::CleanupRefsService, cleanup_schedule.merge_request) do |svc|
expect(svc).to receive(:execute).and_return(result)
end
end
end

View file

@ -6,16 +6,9 @@ RSpec.describe ScheduleMergeRequestCleanupRefsWorker do
subject(:worker) { described_class.new }
describe '#perform' do
before do
allow(MergeRequest::CleanupSchedule)
.to receive(:scheduled_merge_request_ids)
.with(described_class::LIMIT)
.and_return([1, 2, 3, 4])
end
it 'does nothing if the database is read-only' do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
expect(MergeRequestCleanupRefsWorker).not_to receive(:bulk_perform_in)
expect(MergeRequestCleanupRefsWorker).not_to receive(:perform_with_capacity)
worker.perform
end
@ -26,25 +19,17 @@ RSpec.describe ScheduleMergeRequestCleanupRefsWorker do
end
it 'does not schedule any merge request clean ups' do
expect(MergeRequestCleanupRefsWorker).not_to receive(:bulk_perform_in)
expect(MergeRequestCleanupRefsWorker).not_to receive(:perform_with_capacity)
worker.perform
end
end
include_examples 'an idempotent worker' do
it 'schedules MergeRequestCleanupRefsWorker to be performed by batch' do
expect(MergeRequestCleanupRefsWorker)
.to receive(:bulk_perform_in)
.with(
described_class::DELAY,
[[1], [2], [3], [4]],
batch_size: described_class::BATCH_SIZE
)
it 'schedules MergeRequestCleanupRefsWorker to be performed with capacity' do
expect(MergeRequestCleanupRefsWorker).to receive(:perform_with_capacity).twice
expect(worker).to receive(:log_extra_metadata_on_done).with(:merge_requests_count, 4)
worker.perform
subject
end
end
end