diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-15 09:08:57 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-15 09:08:57 +0000 |
commit | 45a8c43afe8a17de19a92708b380b29b6ae04ce6 (patch) | |
tree | 4104e6ac741fbbdeefe9b8b699650a06c14e9056 /app | |
parent | 6bc327a3491069240bd73cc83e17b3078c4148b0 (diff) | |
download | gitlab-ce-45a8c43afe8a17de19a92708b380b29b6ae04ce6.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
43 files changed, 484 insertions, 179 deletions
diff --git a/app/assets/javascripts/alerts_settings/constants.js b/app/assets/javascripts/alerts_settings/constants.js index de7240009bc..6d914fe8361 100644 --- a/app/assets/javascripts/alerts_settings/constants.js +++ b/app/assets/javascripts/alerts_settings/constants.js @@ -185,6 +185,6 @@ export const I18N_ALERT_SETTINGS_FORM = { export const NO_ISSUE_TEMPLATE_SELECTED = { value: '', text: __('No template selected') }; export const TAKING_INCIDENT_ACTION_DOCS_LINK = - '/help/operations/metrics/alerts#trigger-actions-from-alerts'; + '/help/operations/incident_management/alerts#trigger-actions-from-alerts'; export const ISSUE_TEMPLATES_DOCS_LINK = '/help/user/project/description_templates#create-an-issue-template'; diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue index c23a0b866d3..b4b468987d8 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue @@ -1,12 +1,14 @@ <script> -import { GlFormGroup, GlAccordionItem, GlFormInput } from '@gitlab/ui'; +import { GlFormGroup, GlAccordionItem, GlFormInput, GlFormTextarea } from '@gitlab/ui'; import { i18n } from '../constants'; export default { i18n, + placeholderText: i18n.ENTRYPOINT_PLACEHOLDER_TEXT, components: { GlAccordionItem, GlFormInput, + GlFormTextarea, GlFormGroup, }, props: { @@ -15,31 +17,34 @@ export default { required: true, }, }, + computed: { + imageEntryPoint() { + return this.job.image.entrypoint.join('\n'); + }, + }, }; </script> <template> <gl-accordion-item :title="$options.i18n.IMAGE"> - <div class="gl-display-flex"> - <gl-form-group - class="gl-flex-grow-1 gl-flex-basis-half gl-mr-3" - :label="$options.i18n.IMAGE_NAME" - > - <gl-form-input - :value="job.image.name" - data-testid="image-name-input" - @input="$emit('update-job', 'image.name', $event)" - /> - </gl-form-group> - <gl-form-group - class="gl-flex-grow-1 gl-flex-basis-half" - :label="$options.i18n.IMAGE_ENTRYPOINT" - > - <gl-form-input - :value="job.image.entrypoint.join(' ')" - data-testid="image-entrypoint-input" - @input="$emit('update-job', 'image.entrypoint', $event.split(' '))" - /> - </gl-form-group> - </div> + <gl-form-group :label="$options.i18n.IMAGE_NAME"> + <gl-form-input + :value="job.image.name" + data-testid="image-name-input" + @input="$emit('update-job', 'image.name', $event)" + /> + </gl-form-group> + <gl-form-group + :label="$options.i18n.IMAGE_ENTRYPOINT" + :description="$options.i18n.ARRAY_FIELD_DESCRIPTION" + class="gl-mb-0" + > + <gl-form-textarea + :no-resize="false" + :placeholder="$options.placeholderText" + data-testid="image-entrypoint-input" + :value="imageEntryPoint" + @input="$emit('update-job', 'image.entrypoint', $event.split('\n'))" + /> + </gl-form-group> </gl-accordion-item> </template> diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue index b49355d539c..511003d3ad4 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue @@ -39,6 +39,7 @@ export default { availableStages: { type: Array, required: true, + default: () => [], }, }, }; diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue index fd669adb077..9bada3ef110 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue @@ -4,7 +4,7 @@ import { i18n } from '../constants'; export default { i18n, - placeholderText: i18n.ENTRPOINT_PLACEHOLDER_TEXT, + placeholderText: i18n.ENTRYPOINT_PLACEHOLDER_TEXT, components: { GlAccordionItem, GlFormGroup, @@ -38,7 +38,7 @@ export default { }, serviceEntryPoint(service) { const { entrypoint = [''] } = service; - return entrypoint.join(','); + return entrypoint.join('\n'); }, }, }; @@ -75,7 +75,7 @@ export default { :placeholder="$options.placeholderText" :data-testid="`service-entrypoint-input-${index}`" :value="serviceEntryPoint(service)" - @input="$emit('update-job', `services[${index}].entrypoint`, $event.split(','))" + @input="$emit('update-job', `services[${index}].entrypoint`, $event.split('\n'))" /> </gl-form-group> </div> diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js index d9d1a31ab6d..e93a9e84302 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js +++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js @@ -1,4 +1,4 @@ -import { __, s__, sprintf } from '~/locale'; +import { __, s__ } from '~/locale'; export const DRAWER_CONTAINER_CLASS = '.content-wrapper'; @@ -87,14 +87,8 @@ export const JOB_TEMPLATE = { ], }; -export const ENTRYPOINT_PLACEHOLDER_EXAMPLE = - "['/bin/bash','-c','ln -snf /bin/bash /bin/sh && /bin/bash -c $0']"; - -export const ENTRYPOINT_PLACEHOLDER_INPUT = - 'bin/bash,-c,ln -snf /bin/bash /bin/sh && /bin/bash -c $0'; - export const i18n = { - ARRAY_FIELD_DESCRIPTION: s__('JobAssistant|Please use "," to separate array type fields.'), + ARRAY_FIELD_DESCRIPTION: s__('JobAssistant|Please separate array type fields with new lines'), INPUT_FORMAT: s__('JobAssistant|Input format'), ADD_JOB: s__('JobAssistant|Add job'), SCRIPT: s__('JobAssistant|Script'), @@ -117,17 +111,8 @@ export const i18n = { ALLOW_FAILURE: s__('JobAssistant|Allow failure'), INVALID_START_IN: s__('JobAssistant|Error - Valid value is between 1 second and 1 week'), ADD_SERVICE: s__('JobAssistant|Add service'), - SERVICE: s__('JobAssistant|Service'), + SERVICE: s__('JobAssistant|Services'), SERVICE_NAME: s__('JobAssistant|Service name (optional)'), SERVICE_ENTRYPOINT: s__('JobAssistant|Service entrypoint (optional)'), - ENTRPOINT_PLACEHOLDER_TEXT: sprintf( - s__( - 'JobAssistant|Example: %{entrypointPlaceholderExample}, Input format: %{entrypointPlaceholderInput}', - ), - { - entrypointPlaceholderExample: ENTRYPOINT_PLACEHOLDER_EXAMPLE, - entrypointPlaceholderInput: ENTRYPOINT_PLACEHOLDER_INPUT, - }, - false, - ), + ENTRYPOINT_PLACEHOLDER_TEXT: s__('JobAssistant|Please enter the parameters.'), }; diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index e6ee6b702bb..c566490dcb5 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -25,6 +25,13 @@ export default class Project { $(this).parents('.auto-devops-implicitly-enabled-banner').remove(); return e.preventDefault(); }); + $('.hide-mobile-devops-promo').on('click', function (e) { + const projectId = $(this).data('project-id'); + const cookieKey = `hide_mobile_devops_promo_${projectId}`; + setCookie(cookieKey, 'false'); + $(this).parents('#mobile-devops-promo-banner').remove(); + return e.preventDefault(); + }); } static changeProject(url) { diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 494b8c5621d..903c8c214ae 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -45,6 +45,24 @@ class Groups::MilestonesController < Groups::ApplicationController Milestones::UpdateService.new(@milestone.parent, current_user, milestone_params).execute(@milestone) redirect_to milestone_path(@milestone) + rescue ActiveRecord::StaleObjectError + respond_to do |format| + format.html do + @conflict = true + render :edit + end + + format.json do + render json: { + errors: [ + format( + _("Someone edited this %{model_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."), # rubocop:disable Layout/LineLength + model_name: _('milestone') + ) + ] + }, status: :conflict + end + end end def destroy @@ -63,7 +81,15 @@ class Groups::MilestonesController < Groups::ApplicationController end def milestone_params - params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event) + params.require(:milestone) + .permit( + :description, + :due_date, + :lock_version, + :start_date, + :state_event, + :title + ) end def milestones diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 78108cf3478..569a514b23b 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -85,6 +85,24 @@ class Projects::MilestonesController < Projects::ApplicationController end end end + rescue ActiveRecord::StaleObjectError + respond_to do |format| + format.html do + @conflict = true + render :edit + end + + format.json do + render json: { + errors: [ + format( + _("Someone edited this %{model_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."), + model_name: _('milestone') + ) + ] + }, status: :conflict + end + end end def promote @@ -152,7 +170,15 @@ class Projects::MilestonesController < Projects::ApplicationController end def milestone_params - params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event) + params.require(:milestone) + .permit( + :description, + :due_date, + :lock_version, + :start_date, + :state_event, + :title + ) end def search_params diff --git a/app/finders/template_finder.rb b/app/finders/template_finder.rb index b82b601541c..c6c5c30cbf7 100644 --- a/app/finders/template_finder.rb +++ b/app/finders/template_finder.rb @@ -16,16 +16,27 @@ class TemplateFinder def build(type, project, params = {}) if type.to_s == 'licenses' LicenseTemplateFinder.new(project, params) # rubocop: disable CodeReuse/Finder - else + elsif type_allowed?(type) new(type, project, params) end end def all_template_names(project, type) - return {} if !VENDORED_TEMPLATES.key?(type.to_s) && type.to_s != 'licenses' + return {} unless type_allowed?(type) build(type, project).template_names end + + def type_allowed?(type) + case type.to_s + when 'licenses' + true + when 'metrics_dashboard_ymls' + !Feature.enabled?(:remove_monitor_metrics) + else + VENDORED_TEMPLATES.key?(type) + end + end end attr_reader :type, :project, :params diff --git a/app/graphql/mutations/metrics/dashboard/annotations/create.rb b/app/graphql/mutations/metrics/dashboard/annotations/create.rb index 225d313c487..296efa19bb7 100644 --- a/app/graphql/mutations/metrics/dashboard/annotations/create.rb +++ b/app/graphql/mutations/metrics/dashboard/annotations/create.rb @@ -75,6 +75,8 @@ module Mutations private def ready?(**args) + raise_resource_not_available_error! if Feature.enabled?(:remove_monitor_metrics) + # Raise error if both cluster_id and environment_id are present or neither is present unless args[:cluster_id].present? ^ args[:environment_id].present? raise Gitlab::Graphql::Errors::ArgumentError, ANNOTATION_SOURCE_ARGUMENT_ERROR diff --git a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb index 0ee2791f78b..32047cda213 100644 --- a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb +++ b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb @@ -14,6 +14,8 @@ module Mutations description: 'Global ID of the annotation to delete.' def resolve(id:) + raise_resource_not_available_error! if Feature.enabled?(:remove_monitor_metrics) + annotation = authorized_find!(id: id) result = ::Metrics::Dashboard::Annotations::DeleteService.new(context[:current_user], annotation).execute diff --git a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb index 9d6b0486c04..aad9bbebafb 100644 --- a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb +++ b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb @@ -17,6 +17,7 @@ module Resolvers alias_method :dashboard, :object def resolve(**args) + return if Feature.enabled?(:remove_monitor_metrics) return [] unless dashboard ::Metrics::Dashboards::AnnotationsFinder.new(dashboard: dashboard, params: args).execute diff --git a/app/graphql/resolvers/projects/branches_tipping_at_commit_resolver.rb b/app/graphql/resolvers/projects/branches_tipping_at_commit_resolver.rb deleted file mode 100644 index 7e2661f3f77..00000000000 --- a/app/graphql/resolvers/projects/branches_tipping_at_commit_resolver.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Resolvers - module Projects - class BranchesTippingAtCommitResolver < RefTippingAtCommitResolver - MAX_LIMIT = 100 - - calls_gitaly! - - type ::Types::Projects::CommitParentNamesType, null: true - - # the methode ref_prefix is implemented - # because this class is prepending Resolver::CommitParentNamesResolver module - # through it's parent ::Resolvers::RefTippingAtCommitResolver - def ref_prefix - Gitlab::Git::BRANCH_REF_PREFIX - end - end - end -end diff --git a/app/graphql/resolvers/projects/commit_parent_names_resolver.rb b/app/graphql/resolvers/projects/commit_parent_names_resolver.rb deleted file mode 100644 index f52776d715a..00000000000 --- a/app/graphql/resolvers/projects/commit_parent_names_resolver.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -module Resolvers - module Projects - module CommitParentNamesResolver - extend ActiveSupport::Concern - - prepended do - argument :commit_sha, GraphQL::Types::String, - required: true, - description: 'Project commit SHA identifier. For example, `287774414568010855642518513f085491644061`.' - - argument :limit, GraphQL::Types::Int, - required: false, - description: 'Number of branch names to return.' - - alias_method :project, :object - end - - def compute_limit(limit) - max = self.class::MAX_LIMIT - - limit ? [limit, max].min : max - end - - def get_tipping_refs(project, sha, limit: 0) - # the methode ref_prefix needs to be implemented in all classes prepending this module - refs = project.repository.refs_by_oid(oid: sha, ref_patterns: [ref_prefix], limit: limit) - refs.map { |n| n.delete_prefix(ref_prefix) } - end - end - end -end diff --git a/app/graphql/resolvers/projects/commit_references_resolver.rb b/app/graphql/resolvers/projects/commit_references_resolver.rb new file mode 100644 index 00000000000..ca25bad468c --- /dev/null +++ b/app/graphql/resolvers/projects/commit_references_resolver.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Resolvers + module Projects + class CommitReferencesResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + argument :commit_sha, GraphQL::Types::String, + required: true, + description: 'Project commit SHA identifier. For example, `287774414568010855642518513f085491644061`.' + + authorize :read_commit + + alias_method :project, :object + + calls_gitaly! + + type ::Types::CommitReferencesType, null: true + + def resolve(commit_sha:) + authorized_find!(oid: commit_sha) + end + + def find_object(oid:) + project.repository.commit(oid) + end + end + end +end diff --git a/app/graphql/resolvers/projects/ref_tipping_at_commit_resolver.rb b/app/graphql/resolvers/projects/ref_tipping_at_commit_resolver.rb deleted file mode 100644 index 3259a29ac9c..00000000000 --- a/app/graphql/resolvers/projects/ref_tipping_at_commit_resolver.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module Resolvers - module Projects - class RefTippingAtCommitResolver < BaseResolver - include Gitlab::Graphql::Authorize::AuthorizeResource - prepend CommitParentNamesResolver - - type ::Types::Projects::CommitParentNamesType, null: true - - authorize :read_code - - def resolve(commit_sha:, limit: nil) - final_limit = compute_limit(limit) - - names = get_tipping_refs(project, commit_sha, limit: final_limit) - - { - names: names, - total_count: nil - } - end - end - end -end diff --git a/app/graphql/resolvers/projects/tags_tipping_at_commit_resolver.rb b/app/graphql/resolvers/projects/tags_tipping_at_commit_resolver.rb deleted file mode 100644 index 78ee9c997d5..00000000000 --- a/app/graphql/resolvers/projects/tags_tipping_at_commit_resolver.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Resolvers - module Projects - class TagsTippingAtCommitResolver < RefTippingAtCommitResolver - MAX_LIMIT = 100 - - calls_gitaly! - - type ::Types::Projects::CommitParentNamesType, null: true - - # the methode ref_prefix is implemented - # because this class is prepending Resolver::CommitParentNamesResolver module - # through it's parent ::Resolvers::RefTippingAtCommitResolver - def ref_prefix - Gitlab::Git::TAG_REF_PREFIX - end - end - end -end diff --git a/app/graphql/types/commit_references_type.rb b/app/graphql/types/commit_references_type.rb new file mode 100644 index 00000000000..2844a552f3e --- /dev/null +++ b/app/graphql/types/commit_references_type.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Types + class CommitReferencesType < BaseObject + graphql_name 'CommitReferences' + + authorize :read_commit + + def self.field_for_tipping_refs(field_name, field_description) + field field_name, ::Types::Projects::CommitParentNamesType, + null: true, + calls_gitaly: true, + description: field_description do + argument :limit, GraphQL::Types::Int, + required: true, + default_value: 100, + description: 'Number of ref names to return.', + validates: { numericality: { within: 1..1000 } } + end + end + + def self.field_for_containing_refs(field_name, field_description) + field field_name, ::Types::Projects::CommitParentNamesType, + null: true, + calls_gitaly: true, + description: field_description do + argument :exclude_tipped, GraphQL::Types::Boolean, + required: true, + default_value: false, + description: 'Exclude tipping refs. WARNING: This argument can be confusing, if there is a limit. + for example set the limit to 5 and in the 5 out a total of 25 refs there is 2 tipped refs, + then the method will only 3 refs, even though there is more.' + # rubocop: disable GraphQL/ArgumentUniqueness + argument :limit, GraphQL::Types::Int, + required: true, + default_value: 100, + description: 'Number of ref names to return.', + validates: { numericality: { within: 1..1000 } } + # rubocop: enable GraphQL/ArgumentUniqueness + end + end + + field_for_tipping_refs :tipping_tags, "Get tag names tipping at a given commit." + + field_for_tipping_refs :tipping_branches, "Get branch names tipping at a given commit." + + field_for_containing_refs :containing_tags, "Get tag names containing a given commit." + + field_for_containing_refs :containing_branches, "Get branch names containing a given commit." + + def tipping_tags(limit:) + { names: object.tipping_tags(limit: limit) } + end + + def tipping_branches(limit:) + { names: object.tipping_branches(limit: limit) } + end + + def containing_tags(limit:, exclude_tipped:) + { names: object.tags_containing(limit: limit, exclude_tipped: exclude_tipped) } + end + + def containing_branches(limit:, exclude_tipped:) + { names: object.branches_containing(limit: limit, exclude_tipped: exclude_tipped) } + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 64f7daf09a8..7e436d74dcf 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -86,8 +86,14 @@ module Types mount_mutation Mutations::MergeRequests::SetAssignees mount_mutation Mutations::MergeRequests::SetReviewers mount_mutation Mutations::MergeRequests::ReviewerRereview - mount_mutation Mutations::Metrics::Dashboard::Annotations::Create - mount_mutation Mutations::Metrics::Dashboard::Annotations::Delete + mount_mutation Mutations::Metrics::Dashboard::Annotations::Create, deprecated: { + reason: 'Underlying feature was removed in 16.0', + milestone: '16.0' + } + mount_mutation Mutations::Metrics::Dashboard::Annotations::Delete, deprecated: { + reason: 'Underlying feature was removed in 16.0', + milestone: '16.0' + } mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true mount_mutation Mutations::Notes::Create::DiffNote, calls_gitaly: true mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true diff --git a/app/graphql/types/permission_types/issue.rb b/app/graphql/types/permission_types/issue.rb index b38971b64cd..a76dc88adfc 100644 --- a/app/graphql/types/permission_types/issue.rb +++ b/app/graphql/types/permission_types/issue.rb @@ -8,7 +8,7 @@ module Types abilities :read_issue, :admin_issue, :update_issue, :reopen_issue, :read_design, :create_design, :destroy_design, - :create_note + :create_note, :update_design end end end diff --git a/app/graphql/types/project_statistics_redirect_type.rb b/app/graphql/types/project_statistics_redirect_type.rb new file mode 100644 index 00000000000..c8fec0a54c4 --- /dev/null +++ b/app/graphql/types/project_statistics_redirect_type.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class ProjectStatisticsRedirectType < BaseObject + graphql_name 'ProjectStatisticsRedirect' + + field :repository, GraphQL::Types::String, null: false, + description: 'Redirection Route for repository.' + + field :wiki, GraphQL::Types::String, null: false, + description: 'Redirection Route for wiki.' + + field :build_artifacts, GraphQL::Types::String, null: false, + description: 'Redirection Route for job_artifacts.' + + field :packages, GraphQL::Types::String, null: false, + description: 'Redirection Route for packages.' + + field :snippets, GraphQL::Types::String, null: false, + description: 'Redirection Route for snippets.' + + field :container_registry, GraphQL::Types::String, null: false, + description: 'Redirection Route for container_registry.' + end + # rubocop: enable Graphql/AuthorizeTypes +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 6dfbdf765c8..f8a516501c3 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -214,6 +214,11 @@ module Types null: true, description: 'Statistics of the project.' + field :statistics_details_paths, Types::ProjectStatisticsRedirectType, + null: true, + description: 'Redirects for Statistics of the project.', + calls_gitaly: true + field :repository, Types::RepositoryType, null: true, description: 'Git repository of the project.' @@ -612,15 +617,11 @@ module Types authorize: :read_cycle_analytics, alpha: { milestone: '15.10' } - field :tags_tipping_at_commit, ::Types::Projects::CommitParentNamesType, - null: true, - resolver: Resolvers::Projects::TagsTippingAtCommitResolver, - description: "Get tag names tipping at a given commit." - - field :branches_tipping_at_commit, ::Types::Projects::CommitParentNamesType, - null: true, - resolver: Resolvers::Projects::BranchesTippingAtCommitResolver, - description: "Get branch names tipping at a given commit." + field :commit_references, ::Types::CommitReferencesType, + null: true, + resolver: Resolvers::Projects::CommitReferencesResolver, + alpha: { milestone: '16.0' }, + description: "Get tag names containing a given commit." def timelog_categories object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories) @@ -730,6 +731,19 @@ module Types end end + def statistics_details_paths + root_ref = project.repository.root_ref || project.default_branch_or_main + + { + repository: Gitlab::Routing.url_helpers.project_tree_url(project, root_ref), + wiki: Gitlab::Routing.url_helpers.project_wikis_pages_url(project), + build_artifacts: Gitlab::Routing.url_helpers.project_artifacts_url(project), + packages: Gitlab::Routing.url_helpers.project_packages_url(project), + snippets: Gitlab::Routing.url_helpers.project_snippets_url(project), + container_registry: Gitlab::Routing.url_helpers.project_container_registry_index_url(project) + } + end + private def project diff --git a/app/graphql/types/projects/commit_parent_names_type.rb b/app/graphql/types/projects/commit_parent_names_type.rb index 0aa1ca768e9..39f8f1cdd07 100644 --- a/app/graphql/types/projects/commit_parent_names_type.rb +++ b/app/graphql/types/projects/commit_parent_names_type.rb @@ -7,7 +7,6 @@ module Types graphql_name 'CommitParentNames' field :names, [GraphQL::Types::String], null: true, description: 'Names of the commit parent (branch or tag).' - field :total_count, GraphQL::Types::Int, null: true, description: 'Total of parent branches or tags.' end # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 32331136e28..1e87d2861d4 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -259,6 +259,14 @@ module ProjectsHelper cookies["hide_auto_devops_implicitly_enabled_banner_#{project.id}".to_sym].blank? end + def show_mobile_devops_project_promo?(project) + return false unless ::Feature.enabled?(:mobile_devops_projects_promo, project) + + return false unless (project.project_setting.target_platforms & ::ProjectSetting::ALLOWED_TARGET_PLATFORMS).any? + + cookies["hide_mobile_devops_promo_#{project.id}".to_sym].blank? + end + def no_password_message push_pull_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('topics/git/terminology', anchor: 'pull-and-push') } clone_with_https_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('gitlab-basics/start-using-git', anchor: 'clone-with-https') } diff --git a/app/models/blob_viewer/metrics_dashboard_yml.rb b/app/models/blob_viewer/metrics_dashboard_yml.rb index 4b7a178566c..b63f3022198 100644 --- a/app/models/blob_viewer/metrics_dashboard_yml.rb +++ b/app/models/blob_viewer/metrics_dashboard_yml.rb @@ -11,6 +11,10 @@ module BlobViewer self.file_types = %i(metrics_dashboard) self.binary = false + def self.can_render?(blob, verify_binary: true) + super && !Feature.enabled?(:remove_monitor_metrics) + end + def valid? errors.blank? end diff --git a/app/models/commit.rb b/app/models/commit.rb index b618845705c..6d17d7f495d 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -572,8 +572,43 @@ class Commit } end + def tipping_branches(limit: 0) + tipping_refs(Gitlab::Git::BRANCH_REF_PREFIX, limit: limit) + end + + def tipping_tags(limit: 0) + tipping_refs(Gitlab::Git::TAG_REF_PREFIX, limit: limit) + end + + def branches_containing(limit: 0, exclude_tipped: false) + # WARNING: This argument can be confusing, if there is a limit. + # for example set the limit to 5 and in the 5 out a total of 25 refs there is 2 tipped refs, + # then the method will only 3 refs, even though there is more. + excluded = exclude_tipped ? tipping_branches : [] + + refs = repository.branch_names_contains(id, limit: limit) || [] + refs - excluded + end + + def tags_containing(limit: 0, exclude_tipped: false) + # WARNING: This argument can be confusing, if there is a limit. + # for example set the limit to 5 and in the 5 out a total of 25 refs there is 2 tipped refs, + # then the method will only 3 refs, even though there is more. + excluded = exclude_tipped ? tipping_tags : [] + + refs = repository.tag_names_contains(id, limit: limit) || [] + refs - excluded + end + private + def tipping_refs(ref_prefix, limit: 0) + strong_memoize_with(:tipping_tags, ref_prefix, limit) do + refs = repository.refs_by_oid(oid: id, ref_patterns: [ref_prefix], limit: limit) + refs.map { |n| n.delete_prefix(ref_prefix) } + end + end + def expire_note_etag_cache_for_related_mrs MergeRequest.includes(target_project: :namespace).by_commit_sha(id).find_each(&:expire_note_etag_cache) end diff --git a/app/models/container_registry/data_repair_detail.rb b/app/models/container_registry/data_repair_detail.rb index 09e617e69f5..a2616490905 100644 --- a/app/models/container_registry/data_repair_detail.rb +++ b/app/models/container_registry/data_repair_detail.rb @@ -2,9 +2,15 @@ module ContainerRegistry class DataRepairDetail < ApplicationRecord + include EachBatch + self.table_name = 'container_registry_data_repair_details' self.primary_key = :project_id belongs_to :project, optional: false + + enum status: { ongoing: 0, completed: 1, failed: 2 } + + scope :ongoing_since, ->(threshold) { where(status: :ongoing).where('updated_at < ?', threshold) } end end diff --git a/app/models/project.rb b/app/models/project.rb index d8818d9c174..224193fba08 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -188,6 +188,7 @@ class Project < ApplicationRecord has_one :confluence_integration, class_name: 'Integrations::Confluence' has_one :custom_issue_tracker_integration, class_name: 'Integrations::CustomIssueTracker' has_one :datadog_integration, class_name: 'Integrations::Datadog' + has_one :container_registry_data_repair_detail, class_name: 'ContainerRegistry::DataRepairDetail' has_one :discord_integration, class_name: 'Integrations::Discord' has_one :drone_ci_integration, class_name: 'Integrations::DroneCi' has_one :emails_on_push_integration, class_name: 'Integrations::EmailsOnPush' @@ -740,6 +741,11 @@ class Project < ApplicationRecord topic ? with_topic(topic) : none end + scope :pending_data_repair_analysis, -> do + left_outer_joins(:container_registry_data_repair_detail) + .where(container_registry_data_repair_details: { project_id: nil }) + end + enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } chronic_duration_attr :build_timeout_human_readable, :build_timeout, diff --git a/app/services/jira_connect/sync_service.rb b/app/services/jira_connect/sync_service.rb index 92255711399..497c282072d 100644 --- a/app/services/jira_connect/sync_service.rb +++ b/app/services/jira_connect/sync_service.rb @@ -31,7 +31,9 @@ module JiraConnect jira_response: response&.to_json } - if response && response['errorMessages'].present? + has_errors = response && (response['errorMessage'].present? || response['errorMessages'].present?) + + if has_errors logger.error(message) else logger.info(message) diff --git a/app/views/clusters/clusters/_integrations.html.haml b/app/views/clusters/clusters/_integrations.html.haml index 0f62b640b97..4d36c5094a3 100644 --- a/app/views/clusters/clusters/_integrations.html.haml +++ b/app/views/clusters/clusters/_integrations.html.haml @@ -9,7 +9,7 @@ = prometheus_form.hidden_field :application_type, value: @prometheus_integration.application_type .form-group.gl-form-group - help_text = s_('ClusterIntegration|Allows GitLab to query a specifically configured in-cluster Prometheus for metrics.') - - help_link = link_to(_('More information.'), help_page_path("user/clusters/integrations", anchor: "prometheus-cluster-integration"), target: '_blank', rel: 'noopener noreferrer') + - help_link = link_to(_('More information.'), help_page_path("user/clusters/integrations"), target: '_blank', rel: 'noopener noreferrer') = prometheus_form.gitlab_ui_checkbox_component :enabled, s_('ClusterIntegration|Enable Prometheus integration'), help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link } diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml index 1fd8e12016c..89f460606cb 100644 --- a/app/views/groups/milestones/_form.html.haml +++ b/app/views/groups/milestones/_form.html.haml @@ -1,5 +1,9 @@ = gitlab_ui_form_for [@group, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f| = form_errors(@milestone) + + - if @conflict + = render 'shared/model_version_conflict', model_name: _('milestone'), link_path: group_milestone_path(@group, @milestone) + .form-group = f.label :title, _("Title") = f.text_field :title, maxlength: 255, class: "form-control", data: { qa_selector: "milestone_title_field" }, required: true, autofocus: true @@ -15,6 +19,8 @@ .clearfix .error-alert + = f.hidden_field :lock_version + - if @milestone.new_record? = f.submit _('Create milestone'), data: { qa_selector: "create_milestone_button" }, class: 'gl-mr-2', pajamas_button: true = render Pajamas::ButtonComponent.new(href: group_milestones_path(@group)) do diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml index 98221125443..397f96d6846 100644 --- a/app/views/projects/mattermosts/_team_selection.html.haml +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -2,7 +2,7 @@ This service will be installed on the Mattermost instance at %strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host %hr -= form_for(:mattermost, method: :post, url: project_mattermost_path(@project), html: { class: 'js-requires-input' }) do |f| += gitlab_ui_form_for(:mattermost, method: :post, url: project_mattermost_path(@project), html: { class: 'js-requires-input' }) do |f| %h4 Team %p = @teams.one? ? 'The team' : 'Select the team' @@ -42,5 +42,6 @@ %hr .clearfix .float-right - = link_to _('Cancel'), edit_project_settings_integration_path(@project, @integration), class: 'gl-button btn btn-lg' - = f.submit 'Install', class: 'gl-button btn btn-confirm btn-lg' + = render Pajamas::ButtonComponent.new(href: edit_project_settings_integration_path(@project, @integration)) do + = _('Cancel') + = f.submit s_('MattermostService|Install'), pajamas_button: true diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 91c161e8602..be6f9ac83dc 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,5 +1,9 @@ = gitlab_ui_form_for [@project, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f| = form_errors(@milestone) + + - if @conflict + = render 'shared/model_version_conflict', model_name: _('milestone'), link_path: project_milestone_path(@project, @milestone) + - if @redirect_path.present? = f.hidden_field(:redirect_path, name: :redirect_path, id: :redirect_path, value: @redirect_path) .form-group @@ -18,6 +22,8 @@ .clearfix .error-alert + = f.hidden_field :lock_version + - if @milestone.new_record? = f.submit _('Create milestone'), data: { qa_selector: 'create_milestone_button' }, class: 'gl-mr-2', pajamas_button: true = link_to _('Cancel'), project_milestones_path(@project), class: 'gl-button btn btn-default btn-cancel' diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index e21bf7d318b..8a35db357ee 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -7,6 +7,7 @@ = auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity") = render_if_exists 'shared/ultimate_feature_removal_banner', project: @project += render_if_exists 'shared/promotions/promote_mobile_devops', project: @project = render partial: 'flash_messages', locals: { project: @project } = render 'clusters_deprecation_alert' diff --git a/app/views/shared/_captcha_check.html.haml b/app/views/shared/_captcha_check.html.haml index a10ae655ea6..b3b2ae0d969 100644 --- a/app/views/shared/_captcha_check.html.haml +++ b/app/views/shared/_captcha_check.html.haml @@ -10,7 +10,7 @@ %p = _("We detected potential spam in the %{humanized_resource_name}. Please solve the reCAPTCHA to proceed.") % { humanized_resource_name: humanized_resource_name } -= form_for resource_name, method: method, html: { class: 'recaptcha-form js-recaptcha-form' } do |f| += gitlab_ui_form_for resource_name, method: method, html: { class: 'recaptcha-form js-recaptcha-form' } do |f| .recaptcha -# Create a hidden field for each param of the resource - params[resource_name].each do |field, value| @@ -34,4 +34,4 @@ = yield .row-content-block.footer-block - = f.submit _("Create %{humanized_resource_name}") % { humanized_resource_name: humanized_resource_name }, class: 'gl-button btn btn-confirm' + = f.submit _("Create %{humanized_resource_name}") % { humanized_resource_name: humanized_resource_name }, pajamas_button: true diff --git a/app/views/shared/_model_version_conflict.html.haml b/app/views/shared/_model_version_conflict.html.haml new file mode 100644 index 00000000000..134dcf8db7f --- /dev/null +++ b/app/views/shared/_model_version_conflict.html.haml @@ -0,0 +1,6 @@ += render Pajamas::AlertComponent.new(variant: :danger, + dismissible: false, + alert_options: { class: 'gl-mb-5' }) do |c| + = c.body do + - link_to_model = link_to(model_name, link_path, target: '_blank', rel: 'noopener noreferrer') + = _("Someone edited this %{model_name} at the same time you did. Please check out the %{link_to_model} and make sure your changes will not unintentionally remove theirs.").html_safe % { model_name: model_name, link_to_model: link_to_model } diff --git a/app/views/shared/doorkeeper/applications/_form.html.haml b/app/views/shared/doorkeeper/applications/_form.html.haml index b40e2630011..628a34e1278 100644 --- a/app/views/shared/doorkeeper/applications/_form.html.haml +++ b/app/views/shared/doorkeeper/applications/_form.html.haml @@ -21,4 +21,4 @@ = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: @application, scopes: @scopes, f: f .gl-mt-3 - = f.submit _('Save application'), class: "gl-button btn btn-confirm" + = f.submit _('Save application'), pajamas_button: true diff --git a/app/views/shared/integrations/prometheus/_custom_metrics.html.haml b/app/views/shared/integrations/prometheus/_custom_metrics.html.haml index e5ddc055aef..beeb328aedf 100644 --- a/app/views/shared/integrations/prometheus/_custom_metrics.html.haml +++ b/app/views/shared/integrations/prometheus/_custom_metrics.html.haml @@ -3,7 +3,7 @@ .col-lg-3 %p = s_('PrometheusService|Custom metrics require Prometheus installed on a cluster with environment scope "*" OR a manually configured Prometheus to be available.') - = link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/index.md', anchor: 'adding-custom-metrics'), target: '_blank', rel: "noopener noreferrer" + = link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/index.md'), target: '_blank', rel: "noopener noreferrer" .col-lg-9 = render Pajamas::CardComponent.new(header_options: { class: 'gl-display-flex gl-align-items-center' }, body_options: { class: 'gl-p-0' }, card_options: { class: 'gl-mb-5 custom-monitored-metrics js-panel-custom-monitored-metrics', data: { active_custom_metrics: project_prometheus_metrics_path(project), environments_data: environments_list_data, service_active: "#{integration.active}" } }) do |c| diff --git a/app/views/shared/integrations/prometheus/_metrics.html.haml b/app/views/shared/integrations/prometheus/_metrics.html.haml index 1c54e4bd1de..7cd4eeee5f8 100644 --- a/app/views/shared/integrations/prometheus/_metrics.html.haml +++ b/app/views/shared/integrations/prometheus/_metrics.html.haml @@ -34,5 +34,5 @@ .flash-notice .flash-text = html_escape(s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries.")) % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>".html_safe } - = link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/dashboards/variables.md', anchor: 'query-variables') + = link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/index.md') %ul.list-unstyled.metrics-list.js-missing-var-metrics-list diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 58d0a39578a..b6bd691213c 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -6,14 +6,8 @@ = form_errors(issuable) - if @conflict - = render Pajamas::AlertComponent.new(variant: :danger, - dismissible: false, - alert_options: { class: 'gl-mb-5' }) do |c| - = c.body do - Someone edited the #{issuable.class.model_name.human.downcase} the same time you did. - Please check out - = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project, issuable]), target: "_blank", rel: 'noopener noreferrer' - and make sure your changes will not unintentionally remove theirs + - model_name = _(issuable.class.model_name.human.downcase) + = render 'shared/model_version_conflict', model_name: model_name, link_path: polymorphic_path([@project, issuable]) = render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 3eb80292a2f..1149f64314e 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -165,6 +165,15 @@ :weight: 1 :idempotent: true :tags: [] +- :name: container_repository:container_registry_record_data_repair_detail + :worker_name: ContainerRegistry::RecordDataRepairDetailWorker + :feature_category: :container_registry + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: container_repository:delete_container_repository :worker_name: DeleteContainerRepositoryWorker :feature_category: :container_registry diff --git a/app/workers/container_registry/cleanup_worker.rb b/app/workers/container_registry/cleanup_worker.rb index a838b97b35d..448a16ad309 100644 --- a/app/workers/container_registry/cleanup_worker.rb +++ b/app/workers/container_registry/cleanup_worker.rb @@ -12,14 +12,17 @@ module ContainerRegistry feature_category :container_registry STALE_DELETE_THRESHOLD = 30.minutes.freeze + STALE_REPAIR_DETAIL_THRESHOLD = 2.hours.freeze BATCH_SIZE = 200 def perform log_counts reset_stale_deletes + delete_stale_ongoing_repair_details enqueue_delete_container_repository_jobs if ContainerRepository.delete_scheduled.exists? + enqueue_record_repair_detail_jobs if should_enqueue_record_detail_jobs? end private @@ -33,10 +36,31 @@ module ContainerRegistry end end + def delete_stale_ongoing_repair_details + # Deleting stale ongoing repair details would put the project back to the analysis pool + ContainerRegistry::DataRepairDetail + .ongoing_since(STALE_REPAIR_DETAIL_THRESHOLD.ago) + .each_batch(of: BATCH_SIZE) do |batch| # rubocop:disable Style/SymbolProc + batch.delete_all + end + end + def enqueue_delete_container_repository_jobs ContainerRegistry::DeleteContainerRepositoryWorker.perform_with_capacity end + def enqueue_record_repair_detail_jobs + ContainerRegistry::RecordDataRepairDetailWorker.perform_with_capacity + end + + def should_enqueue_record_detail_jobs? + return false unless Gitlab.com? + return false unless Feature.enabled?(:registry_data_repair_worker) + return false unless ContainerRegistry::GitlabApiClient.supports_gitlab_api? + + Project.pending_data_repair_analysis.exists? + end + def log_counts ::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries do log_extra_metadata_on_done( diff --git a/app/workers/container_registry/record_data_repair_detail_worker.rb b/app/workers/container_registry/record_data_repair_detail_worker.rb new file mode 100644 index 00000000000..f400568a3ef --- /dev/null +++ b/app/workers/container_registry/record_data_repair_detail_worker.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module ContainerRegistry + class RecordDataRepairDetailWorker + include ApplicationWorker + include ExclusiveLeaseGuard + include LimitedCapacity::Worker + include Gitlab::Utils::StrongMemoize + + data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency + queue_namespace :container_repository + feature_category :container_registry + urgency :low + worker_resource_boundary :unknown + idempotent! + + MAX_CAPACITY = 2 + LEASE_TIMEOUT = 1.hour.to_i + + def perform_work + return unless Gitlab.com? + return unless next_project + return if next_project.container_registry_data_repair_detail + + missing_count = 0 + + try_obtain_lease do + detail = create_data_repair_detail + + GitlabApiClient.each_sub_repositories_with_tag_page(path: next_project.full_path, + page_size: 50) do |repositories| + next if repositories.empty? + + paths = repositories.map { |repo| ContainerRegistry::Path.new(repo["path"]) } + paths, invalid_paths = paths.partition(&:valid?) + unless invalid_paths.empty? + log_extra_metadata_on_done( + :invalid_paths_parsed_in_container_repository_repair, + invalid_paths.join(' ,') + ) + end + + found_repositories = next_project.container_repositories.where(name: paths.map(&:repository_name)) # rubocop:disable CodeReuse/ActiveRecord + + missing_count += repositories.count - found_repositories.count + end + detail.update!(missing_count: missing_count, status: :completed) + end + rescue StandardError => exception + next_project.reset.container_registry_data_repair_detail&.update(status: :failed) + Gitlab::ErrorTracking.log_exception(exception, class: self.class.name) + end + + def remaining_work_count + return 0 unless Gitlab.com? + return 0 unless Feature.enabled?(:registry_data_repair_worker) + return 0 unless ContainerRegistry::GitlabApiClient.supports_gitlab_api? + + Project.pending_data_repair_analysis.limit(max_running_jobs + 1).count + end + + def max_running_jobs + MAX_CAPACITY + end + + private + + def next_project + Project.pending_data_repair_analysis.first + end + strong_memoize_attr :next_project + + def create_data_repair_detail + ContainerRegistry::DataRepairDetail.create!(project: next_project, status: :ongoing) + end + + # Used by ExclusiveLeaseGuard + def lease_key + "container_registry_data_repair_detail_worker:#{next_project.id}" + end + + # Used by ExclusiveLeaseGuard + def lease_timeout + LEASE_TIMEOUT + end + end +end |