diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-14 15:09:23 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-14 15:09:23 +0300 |
commit | 0b194c4854f312e36616fccf7c610cb2b0ec6957 (patch) | |
tree | f4c3d8ed1cd799e50b979035506675a1d90ad4a9 /app | |
parent | c1e7698dff17b737299127ecf484443e676cdd4f (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
26 files changed, 387 insertions, 62 deletions
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue index 3daa5eebcb6..cb7e3ef9632 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue @@ -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> diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue index 1c4413bef71..0b0560f63c1 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue @@ -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, diff --git a/app/assets/javascripts/repository/components/blob_button_group.vue b/app/assets/javascripts/repository/components/blob_button_group.vue index 424dc4529ff..273825b996a 100644 --- a/app/assets/javascripts/repository/components/blob_button_group.vue +++ b/app/assets/javascripts/repository/components/blob_button_group.vue @@ -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> diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index c3876a77ec4..09ac60c94c7 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -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> diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue new file mode 100644 index 00000000000..6599d99d7bd --- /dev/null +++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue @@ -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> diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js index 22349261d3c..2d2faa8d9f3 100644 --- a/app/assets/javascripts/repository/constants.js +++ b/app/assets/javascripts/repository/constants.js @@ -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'); diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql index 1889f2269f5..a8f263941e2 100644 --- a/app/assets/javascripts/repository/queries/blob_info.query.graphql +++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql @@ -4,6 +4,7 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) { pushCode } repository { + empty blobs(paths: [$filePath]) { nodes { webPath diff --git a/app/assets/javascripts/vuex_shared/bindings.js b/app/assets/javascripts/vuex_shared/bindings.js index 741690886b7..bc3741a3880 100644 --- a/app/assets/javascripts/vuex_shared/bindings.js +++ b/app/assets/javascripts/vuex_shared/bindings.js @@ -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]; diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index 79b69e11b35..a497f56f3b8 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -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); diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss index 45fbd8607fc..a94169ab494 100644 --- a/app/assets/stylesheets/themes/theme_helper.scss +++ b/app/assets/stylesheets/themes/theme_helper.scss @@ -185,7 +185,7 @@ &.active a, .fly-out-top-item-container { background-color: $purple-900; - color: $white; + color: var(--black, $white); } } } diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index c6c9237292d..08066acb45c 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -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]) diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index b5cfc3990b2..475c9de2503 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -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] diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 14783882f5e..e9a75babb97 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -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 diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 1cbde1871d4..ec8ed3d6e7f 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -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 diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index dc37d73df85..c8f6b9aaedb 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -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') diff --git a/app/models/issue.rb b/app/models/issue.rb index 3b236620ed6..7926c4be489 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -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? diff --git a/app/models/merge_request/cleanup_schedule.rb b/app/models/merge_request/cleanup_schedule.rb index 79817269be2..35194b2b318 100644 --- a/app/models/merge_request/cleanup_schedule.rb +++ b/app/models/merge_request/cleanup_schedule.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index e850494ab27..21d5b083476 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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 diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 4351a66351d..d6e7f165d72 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -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") diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml index 206d5edbf84..f45e6c5e8e9 100644 --- a/app/views/admin/application_settings/_usage.html.haml +++ b/app/views/admin/application_settings/_usage.html.haml @@ -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 diff --git a/app/views/admin/dev_ops_report/_callout.html.haml b/app/views/admin/dev_ops_report/_callout.html.haml index f313865478d..2b4c258a00c 100644 --- a/app/views/admin/dev_ops_report/_callout.html.haml +++ b/app/views/admin/dev_ops_report/_callout.html.haml @@ -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') diff --git a/app/views/projects/_invite_members.html.haml b/app/views/projects/_invite_members_empty_project.html.haml index fc292da6fcf..ee2215b0fbb 100644 --- a/app/views/projects/_invite_members.html.haml +++ b/app/views/projects/_invite_members_empty_project.html.haml @@ -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' } } diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 027b81d6c68..0fda74a3be5 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -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') diff --git a/app/views/search/results/_issuable.html.haml b/app/views/search/results/_issuable.html.haml index da0adba88db..551f5c048bc 100644 --- a/app/views/search/results/_issuable.html.haml +++ b/app/views/search/results/_issuable.html.haml @@ -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 diff --git a/app/workers/merge_request_cleanup_refs_worker.rb b/app/workers/merge_request_cleanup_refs_worker.rb index 162c6dc2a88..408d070d56f 100644 --- a/app/workers/merge_request_cleanup_refs_worker.rb +++ b/app/workers/merge_request_cleanup_refs_worker.rb @@ -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) - return unless Feature.enabled?(:merge_request_refs_cleanup, default_enabled: false) + # 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 - merge_request = MergeRequest.find_by_id(merge_request_id) + def perform_work + return unless Feature.enabled?(:merge_request_refs_cleanup, default_enabled: false) 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) + + result = MergeRequests::CleanupRefsService.new(merge_request).execute + + 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 - return if result[:status] == :success + def merge_request + strong_memoize(:merge_request) do + merge_request_cleanup_schedule&.merge_request + end + end - logger.error("Failed cleanup refs of merge request (#{merge_request_id}): #{result[:message]}") + def merge_request_cleanup_schedule + strong_memoize(:merge_request_cleanup_schedule) do + MergeRequest::CleanupSchedule.start_next + end end end diff --git a/app/workers/schedule_merge_request_cleanup_refs_worker.rb b/app/workers/schedule_merge_request_cleanup_refs_worker.rb index b5ea5298879..40a773ca58f 100644 --- a/app/workers/schedule_merge_request_cleanup_refs_worker.rb +++ b/app/workers/schedule_merge_request_cleanup_refs_worker.rb @@ -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 |