Add latest changes from gitlab-org/gitlab@master
2
Gemfile
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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');
|
||||
|
|
|
@ -4,6 +4,7 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) {
|
|||
pushCode
|
||||
}
|
||||
repository {
|
||||
empty
|
||||
blobs(paths: [$filePath]) {
|
||||
nodes {
|
||||
webPath
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -185,7 +185,7 @@
|
|||
&.active a,
|
||||
.fly-out-top-item-container {
|
||||
background-color: $purple-900;
|
||||
color: $white;
|
||||
color: var(--black, $white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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' } }
|
||||
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
·
|
||||
= 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
|
||||
·
|
||||
= 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)
|
||||
·
|
||||
= 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
1
db/schema_migrations/20210705124128
Normal file
|
@ -0,0 +1 @@
|
|||
02aea8fe759614bc3aa751e023aa508963f8183366f6d6f518bbccc2d85ec1a1
|
1
db/schema_migrations/20210706115312
Normal file
|
@ -0,0 +1 @@
|
|||
ac150e706b115849aa3802ae7b8e07d983e89eb637c48582c64948cbc7d7163d
|
1
db/schema_migrations/20210707095545
Normal file
|
@ -0,0 +1 @@
|
|||
98d4deaf0564119c1ee44d76d3a30bff1a0fceb7cab67c5dbef576faef62ddf5
|
1
db/schema_migrations/20210707173645
Normal file
|
@ -0,0 +1 @@
|
|||
e440dac0e14df7309c84e72b98ed6373c712901dc66310a474979e0fce7dc59c
|
1
db/schema_migrations/20210708063032
Normal file
|
@ -0,0 +1 @@
|
|||
77f6db1d2aeebdefd76c96966da6c9e4ce5da2c92a42f6ac2398b35fa21c680f
|
1
db/schema_migrations/20210713070842
Normal file
|
@ -0,0 +1 @@
|
|||
2899d954a199fa52bf6ab4beca5f22dcb9f9f0312e658f1307d1a7355394f1bb
|
|
@ -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);
|
||||
|
||||
|
|
BIN
doc/ci/pipelines/img/coverage_check_approval_rule_14_1.png
Normal file
After Width: | Height: | Size: 114 KiB |
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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>)
|
||||
```
|
||||
|
|
Before Width: | Height: | Size: 22 KiB |
BIN
doc/user/group/import/img/bulk_imports_v14_1.png
Normal file
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 38 KiB |
BIN
doc/user/group/import/img/import_panel_v14_1.png
Normal file
After Width: | Height: | Size: 42 KiB |
|
@ -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)
|
||||
|
|
Before Width: | Height: | Size: 23 KiB |
BIN
doc/user/group/settings/img/import_panel_v14_1.png
Normal file
After Width: | Height: | Size: 42 KiB |
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)**
|
||||
|
||||
|
|
|
@ -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/)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})"
|
||||
|
|
|
@ -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' }
|
||||
|
||||
|
|
|
@ -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/' }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -12,7 +12,7 @@ milestone: "13.9"
|
|||
introduced_by_url:
|
||||
time_frame: 7d
|
||||
data_source:
|
||||
data_category: Operational
|
||||
data_category: Optional
|
||||
distribution:
|
||||
- ee
|
||||
tier:
|
||||
|
|
|
@ -13,7 +13,7 @@ milestone: "13.9"
|
|||
introduced_by_url:
|
||||
time_frame: 7d
|
||||
data_source:
|
||||
data_category: Operational
|
||||
data_category: Optional
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
130
spec/frontend/repository/components/delete_blob_modal_spec.js
Normal 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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
22
spec/migrations/add_upvotes_count_index_to_issues_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|